home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / app.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  95.1 KB  |  2,525 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. import config       # IMPORTANT!! config MUST be imported before downloader
  19. import prefs
  20.  
  21. import database
  22. db = database.defaultDatabase
  23.  
  24. import views
  25. import indexes
  26. import sorts
  27. # import filters
  28. import maps
  29.  
  30. import menu
  31. import util
  32. import feed
  33. import item
  34. import playlist
  35. import tabs
  36.  
  37. import opml
  38. import folder
  39. import autodler
  40. import databaseupgrade
  41. import resources
  42. import selection
  43. import template
  44. import singleclick
  45. import storedatabase
  46. import subscription
  47. import downloader
  48. import autoupdate
  49. import xhtmltools
  50. import guide
  51. import idlenotifier 
  52. import eventloop
  53. import searchengines
  54. import download_utils
  55.  
  56. import os
  57. import re
  58. import shutil
  59. import cgi
  60. import traceback
  61. import threading
  62. import platform
  63. import dialogs
  64. import iconcache
  65. import moviedata
  66. import platformutils
  67. import logging
  68. import theme
  69.  
  70. # These are Python templates for string substitution, not at all
  71. # related to our HTML based templates
  72. from string import Template
  73.  
  74. # Something needs to import this outside of Pyrex. Might as well be app
  75. import templatehelper
  76. import databasehelper
  77. # import fasttypes
  78. import urllib
  79. import menubar # Needed because the XUL port only includes this in pybridge
  80. from gtcache import gettext as _
  81. from gtcache import ngettext
  82. from clock import clock
  83.  
  84. # Global Controller singleton
  85. controller = None
  86.  
  87. # Backend delegate singleton
  88. delegate = None
  89.  
  90. # Run the application. Call this, not start(), on platforms where we
  91. # are responsible for the event loop.
  92. def main():
  93.     platformutils.setupLogging()
  94.     util.setupLogging()
  95.     Controller().Run()
  96.  
  97. # Start up the application and return. Call this, not main(), on
  98. # platform where we are not responsible for the event loop.
  99. def start():
  100.     platformutils.setupLogging()
  101.     util.setupLogging()
  102.     Controller().runNonblocking()
  103.  
  104. def startupFunction(func):
  105.     """Decorator for startup functions.  If they throw an exception, miro will
  106.     show a error dialog and quit.
  107.     """
  108.  
  109.     def wrapped(*args, **kwargs):
  110.         try:
  111.             func(*args, **kwargs)
  112.         except:
  113.             util.failedExn("while finishing starting up")
  114.             frontend.exit(1)
  115.     return wrapped
  116.  
  117. ###############################################################################
  118. #### The Playback Controller base class                                    ####
  119. ###############################################################################
  120.  
  121. class PlaybackControllerBase:
  122.     
  123.     def __init__(self):
  124.         self.currentPlaylist = None
  125.         self.justPlayOne = False
  126.         self.currentItem = None
  127.         self.updateVideoTimeDC = None
  128.  
  129.     def configure(self, view, firstItemId=None, justPlayOne=False):
  130.         self.currentPlaylist = Playlist(view, firstItemId)
  131.         self.justPlayOne = justPlayOne
  132.     
  133.     def reset(self):
  134.         if self.currentPlaylist is not None:
  135.             eventloop.addIdle (self.currentPlaylist.reset, "Reset Playlist")
  136.             self.currentPlaylist = None
  137.  
  138.     def configureWithSelection(self):
  139.         itemSelection = controller.selection.itemListSelection
  140.         view = itemSelection.currentView
  141.         if itemSelection.currentView is None:
  142.             return
  143.  
  144.         for item in view:
  145.             itemid = item.getID()
  146.             if itemSelection.isSelected(view, itemid) and item.isDownloaded():
  147.                 self.configure(view, itemid)
  148.                 break
  149.     
  150.     def enterPlayback(self):
  151.         if self.currentPlaylist is None:
  152.             self.configureWithSelection()
  153.         if self.currentPlaylist is not None:
  154.             startItem = self.currentPlaylist.cur()
  155.             if startItem is not None:
  156.                 self.playItem(startItem)
  157.         
  158.     def exitPlayback(self, switchDisplay=True):
  159.         self.reset()
  160.         if switchDisplay:
  161.             controller.selection.displayCurrentTabContent()
  162.     
  163.     def playPause(self):
  164.         videoDisplay = controller.videoDisplay
  165.         frame = controller.frame
  166.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  167.             videoDisplay.playPause()
  168.         else:
  169.             self.enterPlayback()
  170.  
  171.     def pause(self):
  172.         videoDisplay = controller.videoDisplay
  173.         frame = controller.frame
  174.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  175.             videoDisplay.pause()
  176.  
  177.     def removeItem(self, item):
  178.         if item.idExists():
  179.             item.executeExpire()
  180.  
  181.     def playItem(self, anItem):
  182.         try:
  183.             if self.currentItem:
  184.                 self.currentItem.onViewedCancel()
  185.             self.currentItem = None
  186.             while not os.path.exists(anItem.getVideoFilename()):
  187.                 logging.info ("movie file '%s' is missing, skipping to next",
  188.                               anItem.getVideoFilename())
  189.                 eventloop.addIdle(self.removeItem, "Remove deleted item", args=(anItem.item,))
  190.                 anItem = self.currentPlaylist.getNext()
  191.                 if anItem is None:
  192.                     self.stop()
  193.                     return
  194.  
  195.             self.currentItem = anItem
  196.             if anItem is not None:
  197.                 videoDisplay = controller.videoDisplay
  198.                 videoRenderer = videoDisplay.getRendererForItem(anItem)
  199.                 if videoRenderer is not None:
  200.                     self.playItemInternally(anItem, videoDisplay, videoRenderer)
  201.                 else:
  202.                     frame = controller.frame
  203.                     if frame.getDisplay(frame.mainDisplay) is videoDisplay:
  204.                         if videoDisplay.isFullScreen:
  205.                             videoDisplay.exitFullScreen()
  206.                         videoDisplay.stop()
  207.                     self.scheduleExternalPlayback(anItem)
  208.         except:
  209.             util.failedExn('when trying to play a video')
  210.             self.stop()
  211.  
  212.     def playItemInternally(self, anItem, videoDisplay, videoRenderer):
  213.         logging.info("Playing item with renderer: %s" % videoRenderer)
  214.         controller.videoDisplay.setExternal(False)
  215.         frame = controller.frame
  216.         if frame.getDisplay(frame.mainDisplay) is not videoDisplay:
  217.             frame.selectDisplay(videoDisplay, frame.mainDisplay)
  218.         videoDisplay.selectItem(anItem, videoRenderer)
  219.         if config.get(prefs.RESUME_VIDEOS_MODE) and anItem.resumeTime > 10:
  220.             videoDisplay.playFromTime(anItem.resumeTime)
  221.         else:
  222.             videoDisplay.play()
  223.         self.startUpdateVideoTime()
  224.  
  225.     def playItemExternally(self, itemID):
  226.         anItem = mapToPlaylistItem(db.getObjectByID(int(itemID)))
  227.         controller.videoInfoItem = anItem
  228.         newDisplay = TemplateDisplay('external-playback-continue','default')
  229.         frame = controller.frame
  230.         frame.selectDisplay(newDisplay, frame.mainDisplay)
  231.         return anItem
  232.         
  233.     def scheduleExternalPlayback(self, anItem):
  234.         controller.videoDisplay.setExternal(True)
  235.         controller.videoDisplay.stopOnDeselect = False
  236.         controller.videoInfoItem = anItem
  237.         newDisplay = TemplateDisplay('external-playback','default')
  238.         frame = controller.frame
  239.         frame.selectDisplay(newDisplay, frame.mainDisplay)
  240.         anItem.markItemSeen()
  241.  
  242.     def startUpdateVideoTime(self):
  243.         if not self.updateVideoTimeDC:
  244.             self.updateVideoTimeDC = eventloop.addTimeout(.5, self.updateVideoTime, "Update Video Time")
  245.  
  246.     def stopUpdateVideoTime(self):
  247.         if self.updateVideoTimeDC:
  248.             self.updateVideoTimeDC.cancel()
  249.             self.updateVideoTimeDC = None
  250.  
  251.     def updateVideoTime(self, repeat=True):
  252.         t = controller.videoDisplay.getCurrentTime()
  253.         if t != None and self.currentItem:
  254.             self.currentItem.setResumeTime(t)
  255.         if repeat:
  256.             self.updateVideoTimeDC = eventloop.addTimeout(.5, self.updateVideoTime, "Update Video Time")
  257.  
  258.     def stop(self, switchDisplay=True, markAsViewed=False):
  259.         controller.videoDisplay.setExternal(False)
  260.         if self.updateVideoTimeDC:
  261.             self.updateVideoTime(repeat=False)
  262.             self.stopUpdateVideoTime()
  263.         if self.currentItem:
  264.             self.currentItem.onViewedCancel()
  265.         self.currentItem = None
  266.         frame = controller.frame
  267.         videoDisplay = controller.videoDisplay
  268.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  269.             videoDisplay.stop()
  270.         self.exitPlayback(switchDisplay)
  271.  
  272.     def skip(self, direction, allowMovieReset=True):
  273.         frame = controller.frame
  274.         currentDisplay = frame.getDisplay(frame.mainDisplay)
  275.         if self.currentPlaylist is None:
  276.             self.stop()
  277.         elif (allowMovieReset and direction == -1
  278.                 and hasattr(currentDisplay, 'getCurrentTime') 
  279.                 and currentDisplay.getCurrentTime() > 2.0):
  280.             currentDisplay.goToBeginningOfMovie()
  281.         elif config.get(prefs.SINGLE_VIDEO_PLAYBACK_MODE) or self.justPlayOne:
  282.             self.stop()
  283.         else:
  284.             if direction == 1:
  285.                 nextItem = self.currentPlaylist.getNext()
  286.             else:
  287.                 nextItem = self.currentPlaylist.getPrev()
  288.             if nextItem is None:
  289.                 self.stop()
  290.             else:
  291.                 if self.updateVideoTimeDC:
  292.                     self.updateVideoTime(repeat=False)
  293.                     self.stopUpdateVideoTime()
  294.                 self.playItem(nextItem)
  295.  
  296.     def onMovieFinished(self):
  297.         self.stopUpdateVideoTime()
  298.         setToStart = False
  299.         if self.currentItem:
  300.             self.currentItem.setResumeTime(0)
  301.             if self.currentItem.getFeedURL() == 'dtv:singleFeed':
  302.                 setToStart = True
  303.         if setToStart:
  304.             frame = controller.frame
  305.             currentDisplay = frame.getDisplay(frame.mainDisplay)
  306.             currentDisplay.pause()
  307.             currentDisplay.goToBeginningOfMovie()
  308.             currentDisplay.pause()
  309.         else:
  310.             return self.skip(1, False)
  311.  
  312.  
  313. ###############################################################################
  314. #### Base class for displays                                               ####
  315. #### This must be defined before we import the frontend                    ####
  316. ###############################################################################
  317.  
  318. class Display:
  319.     "Base class representing a display in a MainFrame's right-hand pane."
  320.  
  321.     def __init__(self):
  322.         self.currentFrame = None # tracks the frame that currently has us selected
  323.  
  324.     def isSelected(self):
  325.         return self.currentFrame is not None
  326.  
  327.     def onSelected(self, frame):
  328.         "Called when the Display is shown in the given MainFrame."
  329.         pass
  330.  
  331.     def onDeselected(self, frame):
  332.         """Called when the Display is no longer shown in the given
  333.         MainFrame. This function is called on the Display losing the
  334.         selection before onSelected is called on the Display gaining the
  335.         selection."""
  336.         pass
  337.  
  338.     def onSelected_private(self, frame):
  339.         assert(self.currentFrame == None)
  340.         self.currentFrame = frame
  341.  
  342.     def onDeselected_private(self, frame):
  343.         assert(self.currentFrame == frame)
  344.         self.currentFrame = None
  345.  
  346.     # The MainFrame wants to know if we're ready to display (eg, if the
  347.     # a HTML display has finished loading its contents, so it can display
  348.     # immediately without flicker.) We're to call hook() when we're ready
  349.     # to be displayed.
  350.     def callWhenReadyToDisplay(self, hook):
  351.         hook()
  352.  
  353.     def cancel(self):
  354.         """Called when the Display is not shown because it is not ready yet
  355.         and another display will take its place"""
  356.         pass
  357.  
  358.     def getWatchable(self):
  359.         """Subclasses can implement this if they can return a database view
  360.         of watchable items"""
  361.         return None
  362.  
  363.  
  364. ###############################################################################
  365. #### Provides cross platform part of Video Display                         ####
  366. #### This must be defined before we import the frontend                    ####
  367. ###############################################################################
  368.  
  369. class VideoDisplayBase (Display):
  370.     
  371.     def __init__(self):
  372.         Display.__init__(self)
  373.         self.playbackController = None
  374.         self.volume = 1.0
  375.         self.previousVolume = 1.0
  376.         self.isPlaying = False
  377.         self.isFullScreen = False
  378.         self.isExternal = False
  379.         self.stopOnDeselect = True
  380.         self.renderers = list()
  381.         self.activeRenderer = None
  382.  
  383.     def initRenderers(self):
  384.         pass
  385.  
  386.     def setExternal(self, external):
  387.         self.isExternal = external
  388.  
  389.     def fillMovieData (self, filename, movie_data, callback):
  390.         for renderer in self.renderers:
  391.             success = renderer.fillMovieData(filename, movie_data)
  392.             if success:
  393.                 callback ()
  394.                 return
  395.         callback ()
  396.         
  397.     def getRendererForItem(self, anItem):
  398.         for renderer in self.renderers:
  399.             if renderer.canPlayItem(anItem):
  400.                 return renderer
  401.         return None
  402.  
  403.     def canPlayItem(self, anItem):
  404.         return self.getRendererForItem(anItem) is not None
  405.     
  406.     def canPlayFile(self, filename):
  407.         for renderer in self.renderers:
  408.             if renderer.canPlayFile(filename):
  409.                 return True
  410.         return False
  411.     
  412.     def selectItem(self, anItem, renderer):
  413.         self.stopOnDeselect = True
  414.         controller.videoInfoItem = anItem
  415.         templ = TemplateDisplay('video-info', 'default')
  416.         area = controller.frame.videoInfoDisplay
  417.         controller.frame.selectDisplay(templ, area)
  418.  
  419.         self.setActiveRenderer(renderer)
  420.         self.activeRenderer.selectItem(anItem)
  421.         self.activeRenderer.setVolume(self.getVolume())
  422.  
  423.     def setActiveRenderer (self, renderer):
  424.         self.activeRenderer = renderer
  425.  
  426.     def reset(self):
  427.         self.isPlaying = False
  428.         self.stopOnDeselect = True
  429.         if self.activeRenderer is not None:
  430.             self.activeRenderer.reset()
  431.         self.activeRenderer = None
  432.  
  433.     def goToBeginningOfMovie(self):
  434.         if self.activeRenderer is not None:
  435.             self.activeRenderer.goToBeginningOfMovie()
  436.  
  437.     def playPause(self):
  438.         if self.isPlaying:
  439.             self.pause()
  440.         else:
  441.             self.play()
  442.  
  443.     def playFromTime(self, startTime):
  444.         if self.activeRenderer is not None:
  445.             self.activeRenderer.playFromTime(startTime)
  446.         self.isPlaying = True
  447.  
  448.     def play(self):
  449.         if self.activeRenderer is not None:
  450.             self.activeRenderer.play()
  451.         self.isPlaying = True
  452.  
  453.     def pause(self):
  454.         if self.activeRenderer is not None:
  455.             self.activeRenderer.pause()
  456.         self.isPlaying = False
  457.  
  458.     def stop(self):
  459.         if self.isFullScreen:
  460.             self.exitFullScreen()
  461.         if self.activeRenderer is not None:
  462.             self.activeRenderer.stop()
  463.         self.reset()
  464.  
  465.     def goFullScreen(self):
  466.         self.isFullScreen = True
  467.         if not self.isPlaying:
  468.             self.play()
  469.  
  470.     def exitFullScreen(self):
  471.         self.isFullScreen = False
  472.  
  473.     def getCurrentTime(self):
  474.         if self.activeRenderer is not None:
  475.             return self.activeRenderer.getCurrentTime()
  476.         return None
  477.  
  478.     def setCurrentTime(self, seconds):
  479.         if self.activeRenderer is not None:
  480.             self.activeRenderer.setCurrentTime(seconds)
  481.  
  482.     def getProgress(self):
  483.         if self.activeRenderer is not None:
  484.             return self.activeRenderer.getProgress()
  485.         return 0.0
  486.  
  487.     def setProgress(self, progress):
  488.         if self.activeRenderer is not None:
  489.             return self.activeRenderer.setProgress(progress)
  490.  
  491.     def getDuration(self):
  492.         if self.activeRenderer is not None:
  493.             return self.activeRenderer.getDuration()
  494.         return None
  495.  
  496.     def setVolume(self, level):
  497.         if level > 1.0:
  498.             level = 1.0
  499.         if level < 0.0:
  500.             level = 0.0
  501.         self.volume = level
  502.         config.set(prefs.VOLUME_LEVEL, level)
  503.         if self.activeRenderer is not None:
  504.             self.activeRenderer.setVolume(level)
  505.  
  506.     def getVolume(self):
  507.         return self.volume
  508.  
  509.     def muteVolume(self):
  510.         self.previousVolume = self.getVolume()
  511.         self.setVolume(0.0)
  512.  
  513.     def restoreVolume(self):
  514.         self.setVolume(self.previousVolume)
  515.  
  516.     def onDeselected(self, frame):
  517.         if self.isPlaying and self.stopOnDeselect:
  518.             controller.playbackController.stop(False)
  519.     
  520. ###############################################################################
  521. #### Video renderer base class                                             ####
  522. ###############################################################################
  523.  
  524. class VideoRenderer:
  525.         
  526.     def __init__(self):
  527.         self.interactivelySeeking = False
  528.     
  529.     def canPlayItem(self, anItem):
  530.         return self.canPlayFile (anItem.getVideoFilename())
  531.     
  532.     def canPlayFile(self, filename):
  533.         return False
  534.  
  535.     def fillMovieData(self, filename, movie_data):
  536.         return False
  537.     
  538.     def getDisplayTime(self):
  539.         seconds = self.getCurrentTime()
  540.         return util.formatTimeForUser(seconds)
  541.         
  542.     def getDisplayDuration(self):
  543.         seconds = self.getDuration()
  544.         return util.formatTimeForUser(seconds)
  545.  
  546.     def getDisplayRemainingTime(self):
  547.         seconds = abs(self.getCurrentTime() - self.getDuration())
  548.         return util.formatTimeForUser(seconds, -1)
  549.  
  550.     def getProgress(self):
  551.         duration = self.getDuration()
  552.         if duration == 0 or duration == None:
  553.             return 0.0
  554.         return self.getCurrentTime() / duration
  555.  
  556.     def setProgress(self, progress):
  557.         if progress > 1.0:
  558.             progress = 1.0
  559.         if progress < 0.0:
  560.             progress = 0.0
  561.         self.setCurrentTime(self.getDuration() * progress)
  562.  
  563.     def selectItem(self, anItem):
  564.         self.selectFile (anItem.getVideoFilename())
  565.  
  566.     def selectFile(self, filename):
  567.         pass
  568.         
  569.     def reset(self):
  570.         pass
  571.  
  572.     def setCurrentTime(self, seconds):
  573.         pass
  574.  
  575.     def getDuration(self):
  576.         return 0.0
  577.  
  578.     def setVolume(self, level):
  579.         pass
  580.                 
  581.     def goToBeginningOfMovie(self):
  582.         pass
  583.  
  584.     def getCurrentTime(self):
  585.         return None
  586.         
  587.     def playFromTime(self, position):
  588.         self.play()
  589.         self.setCurrentTime(position)
  590.         
  591.     def play(self):
  592.         pass
  593.         
  594.     def pause(self):
  595.         pass
  596.         
  597.     def stop(self):
  598.         pass
  599.     
  600.     def getRate(self):
  601.         return 1.0
  602.     
  603.     def setRate(self, rate):
  604.         pass
  605.  
  606.     def movieDataProgramInfo(self, videoPath, thumbnailPath):
  607.         raise NotImplementedError()
  608.         
  609. # We can now safely import the frontend module
  610. import frontend
  611.  
  612. ###############################################################################
  613. #### The main application controller object, binding model to view         ####
  614. ###############################################################################
  615.  
  616. class Controller (frontend.Application):
  617.  
  618.     def __init__(self):
  619.         global controller
  620.         global delegate
  621.         frontend.Application.__init__(self)
  622.         assert controller is None
  623.         assert delegate is None
  624.         controller = self
  625.         delegate = frontend.UIBackendDelegate()
  626.         self.frame = None
  627.         self.inQuit = False
  628.         self.guideURL = None
  629.         self.guide = None
  630.         self.initial_feeds = False # True if this is the first run and there's an initial-feeds.democracy file.
  631.         self.finishedStartup = False
  632.         self.idlingNotifier = None
  633.         self.gatheredVideos = None
  634.         self.sendingCrashReport = 0
  635.         self.librarySearchTerm = None
  636.         self.newVideosSearchTerm = None
  637.  
  638.     ### Startup and shutdown ###
  639.  
  640.     def onStartup(self, gatheredVideos=None):
  641.         logging.info ("Starting up %s", config.get(prefs.LONG_APP_NAME))
  642.         logging.info ("Version:    %s", config.get(prefs.APP_VERSION))
  643.         logging.info ("Revision:   %s", config.get(prefs.APP_REVISION))
  644.         logging.info ("Builder:    %s", config.get(prefs.BUILD_MACHINE))
  645.         logging.info ("Build Time: %s", config.get(prefs.BUILD_TIME))
  646.  
  647.         util.print_mem_usage("Pre everything memory check")
  648.         
  649.         logging.info ("Loading preferences...")
  650.  
  651.         config.load()
  652.         config.addChangeCallback(self.configDidChange)
  653.         
  654.         global delegate
  655.         feed.setDelegate(delegate)
  656.         feed.setSortFunc(sorts.item)
  657.         autoupdate.setDelegate(delegate)
  658.         database.setDelegate(delegate)
  659.         dialogs.setDelegate(delegate)
  660.         
  661.         if not config.get(prefs.STARTUP_TASKS_DONE):
  662.             logging.info ("Showing startup dialog...")
  663.             delegate.performStartupTasks(self.finishStartup)
  664.             config.set(prefs.STARTUP_TASKS_DONE, True)
  665.             config.save()
  666.         else:
  667.             self.finishStartup(gatheredVideos)
  668.         logging.info ("Starting event loop thread")
  669.         eventloop.startup()
  670.  
  671.     def finishStartup(self, gatheredVideos=None):
  672.         self.gatheredVideos = gatheredVideos
  673.         eventloop.addUrgentCall(self.initializeDatabase, "Initializing database")
  674.  
  675.     @startupFunction
  676.     def initializeDatabase(self):
  677.         try:
  678.             views.initialize()
  679.             util.print_mem_usage("Pre-database memory check:")
  680.             logging.info ("Restoring database...")
  681.             database.defaultDatabase.liveStorage = storedatabase.LiveStorage()
  682.             db.recomputeFilters()
  683.             eventloop.addUrgentCall(self.checkMoviesDirectoryGone, 
  684.                     "checking movies directory")
  685.         except databaseupgrade.DatabaseTooNewError:
  686.             title = _("Database too new")
  687.             description = Template(_("""\
  688. You have a database that was saved with a newer version of $shortAppName. \
  689. You must download the latest version of $shortAppName and run that.""")).substitute(shortAppName = config.get(prefs.SHORT_APP_NAME))
  690.             def callback(dialog):
  691.                 eventloop.quit()
  692.                 frontend.quit(True)
  693.             dialogs.MessageBoxDialog(title, description).run(callback)
  694.  
  695.     @startupFunction
  696.     def checkMoviesDirectoryGone(self):
  697.         if not self.moviesDirectoryGone():
  698.             eventloop.addUrgentCall(self.finalizeStartup, "finalizing startup")
  699.             return
  700.  
  701.         title = _("Video Directory Missing")
  702.         description = _("""
  703. Miro can't find your primary video directory.  This may be because it's \
  704. located on an external drive that is currently disconnected.
  705.  
  706. If you continue, the video directory will be reset to a location on this \
  707. drive (this will cause you to lose some details about the videos on the \
  708. external drive).  You can also quit, connect the drive, and relaunch Miro.""")
  709.         dialog = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_QUIT,
  710.                 dialogs.BUTTON_LAUNCH_MIRO)
  711.         def callback(dialog):
  712.             if dialog.choice == dialogs.BUTTON_LAUNCH_MIRO:
  713.                 eventloop.addUrgentCall(self.finalizeStartup, "finalizing startup")
  714.             else:
  715.                 eventloop.quit()
  716.                 frontend.quit(True)
  717.         dialog.run(callback)
  718.  
  719.     @startupFunction
  720.     def finalizeStartup(self):
  721.         downloader.startupDownloader()
  722.  
  723.         util.print_mem_usage("Post-downloader memory check")
  724.  
  725.         self.setupGlobalFeed(u'dtv:manualFeed', initiallyAutoDownloadable=False)
  726.         self.setupGlobalFeed(u'dtv:singleFeed', initiallyAutoDownloadable=False)
  727.  
  728.         # Set up the search objects
  729.         self.setupGlobalFeed(u'dtv:search', initiallyAutoDownloadable=False)
  730.         self.setupGlobalFeed(u'dtv:searchDownloads')
  731.  
  732.         # Set up tab list
  733.         tabs.reloadStaticTabs()
  734.         try:
  735.             channelTabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  736.         except LookupError:
  737.             logging.info ("Creating channel tab order")
  738.             channelTabOrder = tabs.TabOrder(u'channel')
  739.         try:
  740.             playlistTabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  741.         except LookupError:
  742.             logging.info ("Creating playlist tab order")
  743.             playlistTabOrder = tabs.TabOrder(u'playlist')
  744.  
  745.         # Set up search engines
  746.         searchengines.createEngines()
  747.  
  748.         # FIXME - channelGuide never gets used.
  749.         (newGuide, channelGuide) = _getInitialChannelGuide()
  750.  
  751.         # This needs to happen after the first channel guide has been created
  752.         _getThemeHistory()
  753.  
  754.         if newGuide:
  755.             if config.get(prefs.MAXIMIZE_ON_FIRST_RUN).lower() not in ['false','no','0']:
  756.                 delegate.maximizeWindow()
  757.             for temp_guide in unicode(config.get(prefs.ADDITIONAL_CHANNEL_GUIDES)).split():
  758.                 if views.guides.getItemWithIndex(indexes.guidesByURL, temp_guide) is None:
  759.                     guide.ChannelGuide(temp_guide)
  760.  
  761.         # Keep a ref of the 'new' and 'download' tabs, we'll need'em later
  762.         self.newTab = None
  763.         self.downloadTab = None
  764.         for tab in views.allTabs:
  765.             if tab.tabTemplateBase == 'newtab':
  766.                 self.newTab = tab
  767.             elif tab.tabTemplateBase == 'downloadtab':
  768.                 self.downloadTab = tab
  769.         views.unwatchedItems.addAddCallback(self.onUnwatchedItemsCountChange)
  770.         views.unwatchedItems.addRemoveCallback(self.onUnwatchedItemsCountChange)
  771.         views.downloadingItems.addAddCallback(self.onDownloadingItemsCountChange)
  772.         views.downloadingItems.addRemoveCallback(self.onDownloadingItemsCountChange)
  773.         self.onUnwatchedItemsCountChange(None, None)
  774.         self.onDownloadingItemsCountChange(None, None)
  775.  
  776.         # If we're missing the file system videos feed, create it
  777.         self.setupGlobalFeed(u'dtv:directoryfeed')
  778.  
  779.         # Start the automatic downloader daemon
  780.         logging.info ("Spawning auto downloader...")
  781.         autodler.startDownloader()
  782.  
  783.         # Start the idle notifier daemon
  784.         if config.get(prefs.LIMIT_UPSTREAM) is True:
  785.             logging.info ("Spawning idle notifier")
  786.             self.idlingNotifier = idlenotifier.IdleNotifier(self)
  787.             self.idlingNotifier.start()
  788.  
  789.         # Set up the playback controller
  790.         self.playbackController = frontend.PlaybackController()
  791.  
  792.         util.print_mem_usage("Pre-UI memory check")
  793.  
  794.         # Put up the main frame
  795.         logging.info ("Displaying main frame...")
  796.         self.frame = frontend.MainFrame(self)
  797.  
  798.         logging.info ("Creating video display...")
  799.         # Set up the video display
  800.         self.videoDisplay = frontend.VideoDisplay()
  801.         self.videoDisplay.initRenderers()
  802.         self.videoDisplay.playbackController = self.playbackController
  803.         self.videoDisplay.setVolume(config.get(prefs.VOLUME_LEVEL))
  804.         util.print_mem_usage("Post-UI memory check")
  805.  
  806.         # create our selection handler
  807.         
  808.         self.selection = selection.SelectionHandler()
  809.  
  810.         self.selection.selectFirstGuide()
  811.  
  812.         if self.initial_feeds:
  813.             views.feedTabs.resetCursor()
  814.             tab = views.feedTabs.getNext()
  815.             if tab is not None:
  816.                 self.selection.selectTabByObject(tab.obj)
  817.  
  818.         util.print_mem_usage("Post-selection memory check")
  819.  
  820.         # Reconnect items to downloaders.
  821.         item.reconnectDownloaders()
  822.  
  823.         util.print_mem_usage("Post-item reconnect memory check")
  824.  
  825.         eventloop.addTimeout (3, autoupdate.checkForUpdates, "Check for updates")
  826.         feed.expireItems()
  827.  
  828.         self.tabDisplay = TemplateDisplay('tablist', 'default',
  829.                 playlistTabOrder=playlistTabOrder,
  830.                 channelTabOrder=channelTabOrder)
  831.         self.frame.selectDisplay(self.tabDisplay, self.frame.channelsDisplay)
  832.  
  833.         # If we have newly available items, provide feedback
  834.         self.updateAvailableItemsCountFeedback()
  835.  
  836.         # Now adding the video files we possibly gathered from the startup
  837.         # dialog
  838.         if self.gatheredVideos is not None and len(self.gatheredVideos) > 0:
  839.             singleclick.resetCommandLineView()
  840.             for v in self.gatheredVideos:
  841.                 try:
  842.                     singleclick.addVideo(v)
  843.                 except Exception, e:
  844.                     logging.info ("error while adding file %s", v)
  845.                     logging.info (e)
  846.  
  847.         util.print_mem_usage("Pre single-click memory check")
  848.  
  849.         # Use an idle for parseCommandLineArgs because the frontend may
  850.         # have put in idle calls to do set up video playback or similar
  851.         # things.
  852.         eventloop.addIdle(singleclick.parseCommandLineArgs, 
  853.                 'parse command line')
  854.  
  855.         util.print_mem_usage("Post single-click memory check")
  856.  
  857.         starttime = clock()
  858.         iconcache.clearOrphans()
  859.         logging.timing ("Icon clear: %.3f", clock() - starttime)
  860.         logging.info ("Starting movie data updates")
  861.         moviedata.movieDataUpdater.startThread()
  862.  
  863.         logging.info ("Finished startup sequence")
  864.         self.finishStartupSequence()
  865.  
  866.     def finishStartupSequence(self):
  867.         self.finishedStartup = True
  868.         frontend.Application.finishStartupSequence(self)
  869.  
  870.     def setupGlobalFeed(self, url, *args, **kwargs):
  871.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  872.         try:
  873.             if feedView.len() == 0:
  874.                 logging.info ("Spawning global feed %s", url)
  875.                 # FIXME - variable d never gets used.
  876.                 d = feed.Feed(url, *args, **kwargs)
  877.             elif feedView.len() > 1:
  878.                 allFeeds = [f for f in feedView]
  879.                 for extra in allFeeds[1:]:
  880.                     extra.remove()
  881.                 util.failed("Too many db objects for %s" % url)
  882.         finally:
  883.             feedView.unlink()
  884.  
  885.     def moviesDirectoryGone(self):
  886.         movies_dir = config.get(prefs.MOVIES_DIRECTORY)
  887.         if not movies_dir.endswith(os.path.sep):
  888.             movies_dir += os.path.sep
  889.         try:
  890.             contents = os.listdir(movies_dir)
  891.         except OSError:
  892.             # We can't access the directory.  Seems like it's gone.
  893.             return True
  894.         if contents != []:
  895.             # There's something inside the directory consider it present  (even
  896.             # if all our items are missing.
  897.             return False
  898.         # make sure that we have actually downloaded something into the movies
  899.         # directory. 
  900.         for downloader in views.remoteDownloads:
  901.             if (downloader.isFinished() and
  902.                     downloader.getFilename().startswith(movies_dir)):
  903.                 return True
  904.         return False
  905.  
  906.     def getGlobalFeed(self, url):
  907.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  908.         rv = feedView[0]
  909.         feedView.unlink()
  910.         return rv
  911.  
  912.     def removeGlobalFeed(self, url):
  913.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  914.         feedView.resetCursor()
  915.         nextfeed = feedView.getNext()
  916.         feedView.unlink()
  917.         if nextfeed is not None:
  918.             logging.info ("Removing global feed %s", url)
  919.             nextfeed.remove()
  920.  
  921.     def copyCurrentFeedURL(self):
  922.         tabs = self.selection.getSelectedTabs()
  923.         if len(tabs) == 1 and tabs[0].isFeed():
  924.             delegate.copyTextToClipboard(tabs[0].obj.getURL())
  925.  
  926.     def recommendCurrentFeed(self):
  927.         tabs = self.selection.getSelectedTabs()
  928.         if len(tabs) == 1 and tabs[0].isFeed():
  929.             # See also dynamic.js if changing this URL
  930.             feed = tabs[0].obj
  931.             query = urllib.urlencode({'url': feed.getURL(), 'title': feed.getTitle()})
  932.             delegate.openExternalURL('http://www.videobomb.com/democracy_channel/email_friend?%s' % (query, ))
  933.  
  934.     def copyCurrentItemURL(self):
  935.         tabs = self.selection.getSelectedItems()
  936.         if len(tabs) == 1 and isinstance(tabs[0], item.Item):
  937.             url = tabs[0].getURL()
  938.             if url:
  939.                 delegate.copyTextToClipboard(url)
  940.  
  941.     def selectAllItems(self):
  942.         self.selection.itemListSelection.selectAll()
  943.         self.selection.setTabListActive(False)
  944.  
  945.     def removeCurrentSelection(self):
  946.         if self.selection.tabListActive:
  947.             selection = self.selection.tabListSelection
  948.         else:
  949.             selection = self.selection.itemListSelection
  950.         seltype = selection.getType()
  951.         if seltype == 'channeltab':
  952.             self.removeCurrentFeed()
  953.         elif seltype == 'addedguidetab':
  954.             self.removeCurrentGuide()
  955.         elif seltype == 'playlisttab':
  956.             self.removeCurrentPlaylist()
  957.         elif seltype == 'item':
  958.             self.removeCurrentItems()
  959.  
  960.     def removeCurrentFeed(self):
  961.         if self.selection.tabListSelection.getType() == 'channeltab':
  962.             feeds = [t.obj for t in self.selection.getSelectedTabs()]
  963.             self.removeFeeds(feeds)
  964.  
  965.     def removeCurrentGuide(self):
  966.         if self.selection.tabListSelection.getType() == 'addedguidetab':
  967.             guides = [t.obj for t in self.selection.getSelectedTabs()]
  968.             if len(guides) != 1:
  969.                 raise AssertionError("Multiple guides selected")
  970.             self.removeGuide(guides[0])
  971.  
  972.     def removeCurrentPlaylist(self):
  973.         if self.selection.tabListSelection.getType() == 'playlisttab':
  974.             playlists = [t.obj for t in self.selection.getSelectedTabs()]
  975.             self.removePlaylists(playlists)
  976.  
  977.     def removeCurrentItems(self):
  978.         if self.selection.itemListSelection.getType() != 'item':
  979.             return
  980.         selected = self.selection.getSelectedItems()
  981.         if self.selection.tabListSelection.getType() != 'playlisttab':
  982.             removable = [i for i in selected if (i.isDownloaded() or i.isExternal()) ]
  983.             if removable:
  984.                 item.expireItems(removable)
  985.         else:
  986.             playlist = self.selection.getSelectedTabs()[0].obj
  987.             for i in selected:
  988.                 playlist.removeItem(i)
  989.  
  990.     def renameCurrentTab(self, typeCheckList=None):
  991.         selected = self.selection.getSelectedTabs()
  992.         if len(selected) != 1:
  993.             return
  994.         obj = selected[0].obj
  995.         if typeCheckList is None:
  996.             typeCheckList = (playlist.SavedPlaylist, folder.ChannelFolder,
  997.                 folder.PlaylistFolder, feed.Feed)
  998.         if obj.__class__ in typeCheckList:
  999.             obj.rename()
  1000.         else:
  1001.             logging.warning ("Bad object type in renameCurrentTab() %s", obj.__class__)
  1002.  
  1003.     def renameCurrentChannel(self):
  1004.         self.renameCurrentTab(typeCheckList=[feed.Feed, folder.ChannelFolder])
  1005.  
  1006.     def renameCurrentPlaylist(self):
  1007.         self.renameCurrentTab(typeCheckList=[playlist.SavedPlaylist,
  1008.                 folder.PlaylistFolder])
  1009.  
  1010.     def downloadCurrentItems(self):
  1011.         selected = self.selection.getSelectedItems()
  1012.         downloadable = [i for i in selected if i.isDownloadable() ]
  1013.         for item in downloadable:
  1014.             item.download()
  1015.  
  1016.     def stopDownloadingCurrentItems(self):
  1017.         selected = self.selection.getSelectedItems()
  1018.         downloading = [i for i in selected if i.getState() == 'downloading']
  1019.         for item in downloading:
  1020.             item.expire()
  1021.  
  1022.     def pauseDownloadingCurrentItems(self):
  1023.         selected = self.selection.getSelectedItems()
  1024.         downloading = [i for i in selected if i.getState() == 'downloading']
  1025.         for item in downloading:
  1026.             item.pause()
  1027.  
  1028.     def updateCurrentFeed(self):
  1029.         for tab in self.selection.getSelectedTabs():
  1030.             if tab.isFeed():
  1031.                 tab.obj.update()
  1032.  
  1033.     def updateAllFeeds(self):
  1034.         for f in views.feeds:
  1035.             f.update()
  1036.  
  1037.     def removeGuide(self, guide):
  1038.         if guide.getDefault():
  1039.             logging.warning ("attempt to remove default guide")
  1040.             return
  1041.         title = _('Remove %s') % guide.getTitle()
  1042.         description = _("Are you sure you want to remove the guide %s?") % (guide.getTitle(),)
  1043.         dialog = dialogs.ChoiceDialog(title, description, 
  1044.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1045.         def dialogCallback(dialog):
  1046.             if guide.idExists() and dialog.choice == dialogs.BUTTON_YES:
  1047.                 guide.remove()
  1048.         dialog.run(dialogCallback)
  1049.  
  1050.     def removePlaylist(self, playlist):
  1051.         return self.removePlaylists([playlist])
  1052.  
  1053.     def removePlaylists(self, playlists):
  1054.         if len(playlists) == 1:
  1055.             title = _('Remove %s') % playlists[0].getTitle()
  1056.             description = _("Are you sure you want to remove %s") % \
  1057.                     playlists[0].getTitle()
  1058.         else:
  1059.             title = _('Remove %s channels') % len(playlists)
  1060.             description = \
  1061.                     _("Are you sure you want to remove these %s playlists") % \
  1062.                     len(playlists)
  1063.         dialog = dialogs.ChoiceDialog(title, description, 
  1064.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1065.         def dialogCallback(dialog):
  1066.             if dialog.choice == dialogs.BUTTON_YES:
  1067.                 for playlist in playlists:
  1068.                     if playlist.idExists():
  1069.                         playlist.remove()
  1070.         dialog.run(dialogCallback)
  1071.  
  1072.     def removeFeed(self, feed):
  1073.         return self.removeFeeds([feed])
  1074.  
  1075.     def removeFeeds(self, feeds):
  1076.         downloads = False
  1077.         downloading = False
  1078.         allDirectories = True
  1079.         for feed in feeds:
  1080.             # We only care about downloaded items in non directory feeds.
  1081.             if isinstance(feed, folder.ChannelFolder) or not feed.getURL().startswith("dtv:directoryfeed"):
  1082.                 allDirectories = False
  1083.                 if feed.hasDownloadedItems():
  1084.                     downloads = True
  1085.                     break
  1086.                 if feed.hasDownloadingItems():
  1087.                     downloading = True
  1088.         if downloads:
  1089.             self.removeFeedsWithDownloads(feeds)
  1090.         elif downloading:
  1091.             self.removeFeedsWithDownloading(feeds)
  1092.         elif allDirectories:
  1093.             self.removeDirectoryFeeds(feeds)
  1094.         else:
  1095.             self.removeFeedsNormal(feeds)
  1096.  
  1097.     def removeFeedsWithDownloads(self, feeds):
  1098.         if len(feeds) == 1:
  1099.             title = _('Remove %s') % feeds[0].getTitle()
  1100.             description = _("""\
  1101. What would you like to do with the videos in this channel that you've \
  1102. downloaded?""")
  1103.         else:
  1104.             title = _('Remove %s channels') % len(feeds)
  1105.             description = _("""\
  1106. What would you like to do with the videos in these channels that you've \
  1107. downloaded?""")
  1108.         dialog = dialogs.ThreeChoiceDialog(title, description, 
  1109.                 dialogs.BUTTON_KEEP_VIDEOS, dialogs.BUTTON_DELETE_VIDEOS,
  1110.                 dialogs.BUTTON_CANCEL)
  1111.         def dialogCallback(dialog):
  1112.             if dialog.choice == dialogs.BUTTON_KEEP_VIDEOS:
  1113.                 manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1114.                 for feed in feeds:
  1115.                     if feed.idExists():
  1116.                         feed.remove(moveItemsTo=manualFeed)
  1117.             elif dialog.choice == dialogs.BUTTON_DELETE_VIDEOS:
  1118.                 for feed in feeds:
  1119.                     if feed.idExists():
  1120.                         feed.remove()
  1121.         dialog.run(dialogCallback)
  1122.  
  1123.     def removeFeedsWithDownloading(self, feeds):
  1124.         if len(feeds) == 1:
  1125.             title = _('Remove %s') % feeds[0].getTitle()
  1126.             description = _("""\
  1127. Are you sure you want to remove %s?  Any downloads in progress will \
  1128. be canceled.""") % feeds[0].getTitle()
  1129.         else:
  1130.             title = _('Remove %s channels') % len(feeds)
  1131.             description = _("""\
  1132. Are you sure you want to remove these %s channels?  Any downloads in \
  1133. progress will be canceled.""") % len(feeds)
  1134.         dialog = dialogs.ChoiceDialog(title, description, 
  1135.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1136.         def dialogCallback(dialog):
  1137.             if dialog.choice == dialogs.BUTTON_YES:
  1138.                 for feed in feeds:
  1139.                     if feed.idExists():
  1140.                         feed.remove()
  1141.         dialog.run(dialogCallback)
  1142.  
  1143.     def removeFeedsNormal(self, feeds):
  1144.         if len(feeds) == 1:
  1145.             title = _('Remove %s') % feeds[0].getTitle()
  1146.             description = _("""\
  1147. Are you sure you want to remove %s?""") % feeds[0].getTitle()
  1148.         else:
  1149.             title = _('Remove %s channels') % len(feeds)
  1150.             description = _("""\
  1151. Are you sure you want to remove these %s channels?""") % len(feeds)
  1152.         dialog = dialogs.ChoiceDialog(title, description, 
  1153.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1154.         def dialogCallback(dialog):
  1155.             if dialog.choice == dialogs.BUTTON_YES:
  1156.                 for feed in feeds:
  1157.                     if feed.idExists():
  1158.                         feed.remove()
  1159.         dialog.run(dialogCallback)
  1160.  
  1161.     def removeDirectoryFeeds(self, feeds):
  1162.         if len(feeds) == 1:
  1163.             title = _('Stop watching %s') % feeds[0].getTitle()
  1164.             description = _("""\
  1165. Are you sure you want to stop watching %s?""") % feeds[0].getTitle()
  1166.         else:
  1167.             title = _('Stop watching %s directories') % len(feeds)
  1168.             description = _("""\
  1169. Are you sure you want to stop watching these %s directories?""") % len(feeds)
  1170.         dialog = dialogs.ChoiceDialog(title, description, 
  1171.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1172.         def dialogCallback(dialog):
  1173.             if dialog.choice == dialogs.BUTTON_YES:
  1174.                 for feed in feeds:
  1175.                     if feed.idExists():
  1176.                         feed.remove()
  1177.         dialog.run(dialogCallback)
  1178.  
  1179.     def playView(self, view, firstItemId=None, justPlayOne=False):
  1180.         self.playbackController.configure(view, firstItemId, justPlayOne)
  1181.         self.playbackController.enterPlayback()
  1182.  
  1183.     def downloaderShutdown(self):
  1184.         logging.info ("Closing Database...")
  1185.         database.defaultDatabase.liveStorage.close()
  1186.         logging.info ("Shutting down event loop")
  1187.         eventloop.quit()
  1188.         logging.info ("Shutting down frontend")
  1189.         frontend.quit()
  1190.  
  1191.     @eventloop.asUrgent
  1192.     def quit(self):
  1193.         global delegate
  1194.         if self.inQuit:
  1195.             return
  1196.         downloadsCount = views.downloadingItems.len()
  1197.             
  1198.         if (downloadsCount > 0 and config.get(prefs.WARN_IF_DOWNLOADING_ON_QUIT)) or (self.sendingCrashReport > 0):
  1199.             title = _("Are you sure you want to quit?")
  1200.             if self.sendingCrashReport > 0:
  1201.                 message = _("Miro is still uploading your crash report. If you quit now the upload will be canceled.  Quit Anyway?")
  1202.                 dialog = dialogs.ChoiceDialog(title, message,
  1203.                                               dialogs.BUTTON_QUIT,
  1204.                                               dialogs.BUTTON_CANCEL)
  1205.             else:
  1206.                 message = ngettext ("You have %d download still in progress.  Quit Anyway?", 
  1207.                                     "You have %d downloads still in progress.  Quit Anyway?", 
  1208.                                     downloadsCount) % (downloadsCount,)
  1209.                 warning = _ ("Warn me when I attempt to quit with downloads in progress")
  1210.                 dialog = dialogs.CheckboxDialog(title, message, warning, True,
  1211.                         dialogs.BUTTON_QUIT, dialogs.BUTTON_CANCEL)
  1212.  
  1213.             def callback(dialog):
  1214.                 if dialog.choice == dialogs.BUTTON_QUIT:
  1215.                     if isinstance(dialog, dialogs.CheckboxDialog):
  1216.                         config.set(prefs.WARN_IF_DOWNLOADING_ON_QUIT,
  1217.                                    dialog.checkbox_value)
  1218.                     self.quitStage2()
  1219.                 else:
  1220.                     self.inQuit = False
  1221.             dialog.run(callback)
  1222.             self.inQuit = True
  1223.         else:
  1224.             self.quitStage2()
  1225.  
  1226.     def quitStage2(self):
  1227.         logging.info ("Shutting down Downloader...")
  1228.         downloader.shutdownDownloader(self.downloaderShutdown)
  1229.  
  1230.     @eventloop.asUrgent
  1231.     def setGuideURL(self, guideURL):
  1232.         """Change the URL of the current channel guide being displayed.  If no
  1233.         guide is being display, pass in None.
  1234.  
  1235.         This method must be called from the onSelectedTabChange in the
  1236.         platform code.  URLs are legal within guideURL will be allow
  1237.         through in onURLLoad().
  1238.         """
  1239.         self.guide = None
  1240.         if guideURL is not None:
  1241.             self.guideURL = guideURL
  1242.             for guideObj in views.guides:
  1243.                 if guideObj.getURL() == controller.guideURL:
  1244.                     self.guide = guideObj
  1245.         else:
  1246.             self.guideURL = None
  1247.  
  1248.     @eventloop.asIdle
  1249.     def setLastVisitedGuideURL(self, url):
  1250.         selectedTabs = self.selection.getSelectedTabs()
  1251.         selectedObjects = [t.obj for t in selectedTabs]
  1252.         if (len(selectedTabs) != 1 or 
  1253.                 not isinstance(selectedObjects[0], guide.ChannelGuide)):
  1254.             logging.warn("setLastVisitedGuideURL called, but a channelguide "
  1255.                     "isn't selected.  Selection: %s" % selectedObjects)
  1256.             return
  1257.         if selectedObjects[0].isPartOfGuide(url) and (
  1258.             url.startswith(u"http://") or url.startswith(u"https://")):
  1259.             selectedObjects[0].lastVisitedURL = url
  1260.         else:
  1261.             logging.warn("setLastVisitedGuideURL called, but the guide is no "
  1262.                     "longer selected")
  1263.  
  1264.     def onShutdown(self):
  1265.         try:
  1266.             eventloop.join()        
  1267.             logging.info ("Saving preferences...")
  1268.             config.save()
  1269.  
  1270. #             logging.info ("Removing search feed")
  1271. #             TemplateActionHandler(None, None).resetSearch()
  1272. #             self.removeGlobalFeed('dtv:search')
  1273.  
  1274.             logging.info ("Shutting down icon cache updates")
  1275.             iconcache.iconCacheUpdater.shutdown()
  1276.             logging.info ("Shutting down movie data updates")
  1277.             moviedata.movieDataUpdater.shutdown()
  1278.  
  1279. #             logging.info ("Removing static tabs...")
  1280. #             views.allTabs.unlink() 
  1281. #             tabs.removeStaticTabs()
  1282.  
  1283.             if self.idlingNotifier is not None:
  1284.                 logging.info ("Shutting down IdleNotifier")
  1285.                 self.idlingNotifier.join()
  1286.  
  1287.             logging.info ("Done shutting down.")
  1288.             logging.info ("Remaining threads are:")
  1289.             for thread in threading.enumerate():
  1290.                 logging.info ("%s", thread)
  1291.  
  1292.         except:
  1293.             util.failedExn("while shutting down")
  1294.             frontend.exit(1)
  1295.  
  1296.     ### Handling config/prefs changes
  1297.     
  1298.     def configDidChange(self, key, value):
  1299.         if key is prefs.LIMIT_UPSTREAM.key:
  1300.             if value is False:
  1301.                 # The Windows version can get here without creating an
  1302.                 # idlingNotifier
  1303.                 try:
  1304.                     self.idlingNotifier.join()
  1305.                 except:
  1306.                     pass
  1307.                 self.idlingNotifier = None
  1308.             elif self.idlingNotifier is None:
  1309.                 self.idlingNotifier = idlenotifier.IdleNotifier(self)
  1310.                 self.idlingNotifier.start()
  1311.  
  1312.     ### Handling system idle events
  1313.     
  1314.     def systemHasBeenIdlingSince(self, seconds):
  1315.         self.setUpstreamLimit(False)
  1316.  
  1317.     def systemIsActiveAgain(self):
  1318.         self.setUpstreamLimit(True)
  1319.  
  1320.     ### Handling events received from the OS (via our base class) ###
  1321.  
  1322.     # Called by Frontend via Application base class in response to OS request.
  1323.     def addAndSelectFeed(self, url = None, showTemplate = None):
  1324.         return GUIActionHandler().addFeed(url, showTemplate)
  1325.  
  1326.     def addAndSelectGuide(self, url = None):
  1327.         return GUIActionHandler().addGuide(url)
  1328.  
  1329.     def addSearchFeed(self, term=None, style=dialogs.SearchChannelDialog.CHANNEL, location = None):
  1330.         return GUIActionHandler().addSearchFeed(term, style, location)
  1331.  
  1332.     def testSearchFeedDialog(self):
  1333.         return GUIActionHandler().testSearchFeedDialog()
  1334.  
  1335.     ### Handling 'DTVAPI' events from the channel guide ###
  1336.  
  1337.     def addFeed(self, url = None):
  1338.         return GUIActionHandler().addFeed(url, selected = None)
  1339.  
  1340.     def selectFeed(self, url):
  1341.         return GUIActionHandler().selectFeed(url)
  1342.  
  1343.     ### Keep track of currently available+downloading items and refresh the
  1344.     ### corresponding tabs accordingly.
  1345.  
  1346.     def onUnwatchedItemsCountChange(self, obj, id):
  1347.         assert self.newTab is not None
  1348.         self.newTab.redraw()
  1349.         self.updateAvailableItemsCountFeedback()
  1350.         if hasattr(frontend.Application, "onUnwatchedItemsCountChange"):
  1351.             frontend.Application.onUnwatchedItemsCountChange(self, obj, id)
  1352.  
  1353.     def onDownloadingItemsCountChange(self, obj, id):
  1354.         assert self.downloadTab is not None
  1355.         self.downloadTab.redraw()
  1356.         if hasattr(frontend.Application, "onDownloadingItemsCountChange"):
  1357.             frontend.Application.onDownloadingItemsCountChange(self, obj, id)
  1358.  
  1359.     def updateAvailableItemsCountFeedback(self):
  1360.         global delegate
  1361.         count = views.unwatchedItems.len()
  1362.         delegate.updateAvailableItemsCountFeedback(count)
  1363.  
  1364.     ### Chrome search:
  1365.     ### Switch to the search tab and perform a search using the specified engine.
  1366.  
  1367.     def performSearch(self, engine, query):
  1368.         util.checkU(engine)
  1369.         util.checkU(query)
  1370.         handler = TemplateActionHandler(None, None)
  1371.         handler.updateLastSearchEngine(engine)
  1372.         handler.updateLastSearchQuery(query)
  1373.         handler.performSearch(engine, query)
  1374.         self.selection.selectTabByTemplateBase('searchtab')
  1375.  
  1376.     ### ----
  1377.  
  1378.     def setUpstreamLimit(self, setLimit):
  1379.         if setLimit:
  1380.             limit = config.get(prefs.UPSTREAM_LIMIT_IN_KBS)
  1381.             # upstream limit should be set here
  1382.         else:
  1383.             # upstream limit should be unset here
  1384.             pass
  1385.  
  1386.     def handleURIDrop(self, data, **kwargs):
  1387.         """Handle an external drag that contains a text/uri-list mime-type.
  1388.         data should be the text/uri-list data, in escaped form.
  1389.  
  1390.         kwargs is thrown away.  It exists to catch weird URLs, like
  1391.         javascript: which sometime result in us getting extra arguments.
  1392.         """
  1393.  
  1394.         lastAddedFeed = None
  1395.         data = urllib.unquote(data)
  1396.         for url in data.split(u"\n"):
  1397.             url = url.strip()
  1398.             if url == u"":
  1399.                 continue
  1400.             if url.startswith(u"file://"):
  1401.                 filename = download_utils.getFileURLPath(url)
  1402.                 filename = platformutils.osFilenameToFilenameType(filename)
  1403.                 eventloop.addIdle (singleclick.openFile,
  1404.                     "Open Dropped file", args=(filename,))
  1405.             elif url.startswith(u"http:") or url.startswith(u"https:"):
  1406.                 url = feed.normalizeFeedURL(url)
  1407.                 if feed.validateFeedURL(url) and not feed.getFeedByURL(url):
  1408.                     lastAddedFeed = feed.Feed(url)
  1409.  
  1410.         if lastAddedFeed:
  1411.             controller.selection.selectTabByObject(lastAddedFeed)
  1412.  
  1413.     def handleDrop(self, dropData, type, sourceData):
  1414.         try:
  1415.             destType, destID = dropData.split("-")
  1416.             if destID == 'END':
  1417.                 destObj = None
  1418.             elif destID == 'START':
  1419.                 if destType == 'channel':
  1420.                     tabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  1421.                 else:
  1422.                     tabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  1423.                 for tab in tabOrder.getView():
  1424.                     destObj = tab.obj
  1425.                     break
  1426.             else:
  1427.                 destObj = db.getObjectByID(int(destID))
  1428.             sourceArea, sourceID = sourceData.split("-")
  1429.             sourceID = int(sourceID)
  1430.             draggedIDs = self.selection.calcSelection(sourceArea, sourceID)
  1431.         except:
  1432.             logging.exception ("error parsing drop (%r, %r, %r)",
  1433.                                dropData, type, sourceData)
  1434.             return
  1435.  
  1436.         if destType == 'playlist' and type == 'downloadeditem':
  1437.             # dropping an item on a playlist
  1438.             destObj.handleDNDAppend(draggedIDs)
  1439.         elif ((destType == 'channelfolder' and type == 'channel') or
  1440.                 (destType == 'playlistfolder' and type == 'playlist')):
  1441.             # Dropping a channel/playlist onto a folder
  1442.             obj = db.getObjectByID(int(destID))
  1443.             obj.handleDNDAppend(draggedIDs)
  1444.         elif (destType in ('playlist', 'playlistfolder') and 
  1445.                 type in ('playlist', 'playlistfolder')):
  1446.             # Reording the playlist tabs
  1447.             tabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  1448.             tabOrder.handleDNDReorder(destObj, draggedIDs)
  1449.         elif (destType in ('channel', 'channelfolder') and
  1450.                 type in ('channel', 'channelfolder')):
  1451.             # Reordering the channel tabs
  1452.             tabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  1453.             tabOrder.handleDNDReorder(destObj, draggedIDs)
  1454.         elif destType == "playlistitem" and type == "downloadeditem":
  1455.             # Reording items in a playlist
  1456.             playlist = self.selection.getSelectedTabs()[0].obj
  1457.             playlist.handleDNDReorder(destObj, draggedIDs)
  1458.         else:
  1459.             logging.info ("Can't handle drop. Dest type: %s Dest id: %s Type: %s",
  1460.                           destType, destID, type)
  1461.  
  1462.     def addToNewPlaylist(self):
  1463.         selected = controller.selection.getSelectedItems()
  1464.         childIDs = [i.getID() for i in selected if i.isDownloaded()]
  1465.         playlist.createNewPlaylist(childIDs)
  1466.  
  1467.     def startUploads(self):
  1468.         selected = controller.selection.getSelectedItems()
  1469.         for i in selected:
  1470.             i.startUpload()
  1471.  
  1472.     def newDownload(self, url = None):
  1473.         return GUIActionHandler().addDownload(url)
  1474.         
  1475.     def importChannels(self):
  1476.         importer = opml.Importer()
  1477.         importer.importSubscriptions()
  1478.     
  1479.     def exportChannels(self):
  1480.         exporter = opml.Exporter()
  1481.         exporter.exportSubscriptions()
  1482.  
  1483. ###############################################################################
  1484. #### TemplateDisplay: a HTML-template-driven right-hand display panel      ####
  1485. ###############################################################################
  1486.  
  1487. class TemplateDisplay(frontend.HTMLDisplay):
  1488.  
  1489.     def __init__(self, templateName, templateState, frameHint=None, areaHint=None, 
  1490.             baseURL=None, *args, **kargs):
  1491.         """'templateName' is the name of the inital template file.  'data' is
  1492.         keys for the template. 'templateState' is a string with the state of the
  1493.         template.
  1494.         """
  1495.  
  1496.         logging.debug ("Processing %s", templateName)
  1497.         self.templateName = templateName
  1498.         self.templateState = templateState
  1499.         (tch, self.templateHandle) = template.fillTemplate(templateName,
  1500.                 self, self.getDTVPlatformName(), self.getEventCookie(),
  1501.                 self.getBodyTagExtra(), templateState = templateState,
  1502.                                                            *args, **kargs)
  1503.         self.args = args
  1504.         self.kargs = kargs
  1505.         html = tch.read()
  1506.  
  1507.         self.actionHandlers = [
  1508.             ModelActionHandler(delegate),
  1509.             GUIActionHandler(),
  1510.             TemplateActionHandler(self, self.templateHandle),
  1511.             ]
  1512.  
  1513.         loadTriggers = self.templateHandle.getTriggerActionURLsOnLoad()
  1514.         newPage = self.runActionURLs(loadTriggers)
  1515.  
  1516.         if newPage:
  1517.             self.templateHandle.unlinkTemplate()
  1518.             # FIXME - url is undefined here!
  1519.             self.__init__(re.compile(r"^template:(.*)$").match(url).group(1), frameHint, areaHint, baseURL)
  1520.         else:
  1521.             frontend.HTMLDisplay.__init__(self, html, frameHint=frameHint, areaHint=areaHint, baseURL=baseURL)
  1522.  
  1523.             self.templateHandle.initialFillIn()
  1524.  
  1525.     def __eq__(self, other):
  1526.         return (other.__class__ == TemplateDisplay and 
  1527.                 self.templateName == other.templateName and 
  1528.                 self.args == other.args and 
  1529.                 self.kargs == other.kargs)
  1530.  
  1531.     def __str__(self):
  1532.         return "Template <%s> args=%s kargs=%s" % (self.templateName, self.args, self.kargs)
  1533.  
  1534.     def reInit(self, *args, **kargs):
  1535.         self.args = args
  1536.         self.kargs = kargs
  1537.         try:
  1538.             self.templateHandle.templateVars['reInit'](*args, **kargs)
  1539.         except:
  1540.             pass
  1541.         self.templateHandle.forceUpdate()
  1542.         
  1543.     def runActionURLs(self, triggers):
  1544.         newPage = False
  1545.         for url in triggers:
  1546.             if url.startswith('action:'):
  1547.                 self.onURLLoad(url)
  1548.             elif url.startswith('template:'):
  1549.                 newPage = True
  1550.                 break
  1551.         return newPage
  1552.  
  1553.     def parseEventURL(self, url):
  1554.         match = re.match(r"[a-zA-Z]+:([^?]+)(\?(.*))?$", url)
  1555.         if match:
  1556.             path = match.group(1)
  1557.             argString = match.group(3)
  1558.             if argString is None:
  1559.                 argString = u''
  1560.             argString = argString.encode('utf8')
  1561.             # argString is turned into a str since parse_qs will fail on utf8 that has been url encoded.
  1562.             argLists = cgi.parse_qs(argString, keep_blank_values=True)
  1563.  
  1564.             # argLists is a dictionary from parameter names to a list
  1565.             # of values given for that parameter. Take just one value
  1566.             # for each parameter, raising an error if more than one
  1567.             # was given.
  1568.             args = {}
  1569.             for key in argLists.keys():
  1570.                 value = argLists[key]
  1571.                 if len(value) != 1:
  1572.                     import template_compiler
  1573.                     raise template_compiler.TemplateError, "Multiple values of '%s' argument passed to '%s' action" % (key, url)
  1574.                 # Cast the value results back to unicode
  1575.                 try:
  1576.                     args[key.encode('ascii','replace')] = value[0].decode('utf8')
  1577.                 except:
  1578.                     args[key.encode('ascii','replace')] = value[0].decode('ascii', 'replace')
  1579.             return path, args
  1580.         else:
  1581.             raise ValueError("Badly formed eventURL: %s" % url)
  1582.  
  1583.  
  1584.     # Returns true if the browser should handle the URL.
  1585.     def onURLLoad(self, url):
  1586.         util.checkU(url)
  1587.         logging.info ("got %s", url)
  1588.         try:
  1589.             # Special-case non-'action:'-format URL
  1590.             if url.startswith (u"template:"):
  1591.                 name, args = self.parseEventURL(url)
  1592.                 self.dispatchAction('switchTemplate', name=name, **args)
  1593.                 return False
  1594.  
  1595.             # Standard 'action:' URL
  1596.             if url.startswith (u"action:"):
  1597.                 action, args = self.parseEventURL(url)
  1598.                 self.dispatchAction(action, **args)
  1599.                 return False
  1600.  
  1601.             # Let channel guide URLs pass through
  1602.             if controller.guide is not None and \
  1603.                    controller.guide.isPartOfGuide(url):
  1604.                 controller.setLastVisitedGuideURL(url)
  1605.                 return True
  1606.             if url.startswith(u'file://'):
  1607.                 path = download_utils.getFileURLPath(url)
  1608.                 return os.path.exists(path)
  1609.  
  1610.             # If we get here, this isn't a DTV URL. We should open it
  1611.             # in an external browser.
  1612.             if (url.startswith(u'http://') or url.startswith(u'https://') or
  1613.                 url.startswith(u'ftp://') or url.startswith(u'mailto:') or
  1614.                 url.startswith(u'feed://')):
  1615.                 self.handleCandidateExternalURL(url)
  1616.                 return False
  1617.  
  1618.         except:
  1619.             details = "Handling action URL '%s'" % (url, )
  1620.             util.failedExn("while handling a request", details = details)
  1621.  
  1622.         return True
  1623.  
  1624.     @eventloop.asUrgent
  1625.     def handleCandidateExternalURL(self, url):
  1626.         """Open a URL that onURLLoad thinks is an external URL.
  1627.         handleCandidateExternalURL does extra checks that onURLLoad can't do
  1628.         because it's happens in the gui thread and can't access the DB.
  1629.         """
  1630.  
  1631.         # check for subscribe.getdemocracy.com links
  1632.         type, subscribeURLs = subscription.findSubscribeLinks(url)
  1633.  
  1634.         # check if the url that came from a guide, but the user switched tabs
  1635.         # before it went through.
  1636.         if len(subscribeURLs) == 0:
  1637.             for guideObj in views.guides:
  1638.                 if guideObj.isPartOfGuide(url):
  1639.                     return
  1640.  
  1641.         normalizedURLs = []
  1642.         for url in subscribeURLs:
  1643.             normalized = feed.normalizeFeedURL(url)
  1644.             if feed.validateFeedURL(normalized):
  1645.                 normalizedURLs.append(normalized)
  1646.         if normalizedURLs:
  1647.             if type == 'feed':
  1648.                 for url in normalizedURLs:
  1649.                     if feed.getFeedByURL(url) is None:
  1650.                         newFeed = feed.Feed(url)
  1651.                         newFeed.blink()
  1652.             elif type == 'download':
  1653.                 for url in normalizedURLs:
  1654.                     filename = platformutils.unicodeToFilename(url)
  1655.                     singleclick.downloadURL(filename)
  1656.             elif type == 'guide':
  1657.                 for url in normalizedURLs:
  1658.                     if guide.getGuideByURL (url) is None:
  1659.                         guide.ChannelGuide(url)
  1660.             else:
  1661.                 raise AssertionError("Unkown subscribe type")
  1662.             return
  1663.  
  1664.         if url.startswith(u'feed://'):
  1665.             url = u"http://" + url[len(u"feed://"):]
  1666.             f = feed.getFeedByURL(url)
  1667.             if f is None:
  1668.                 f = feed.Feed(url)
  1669.             f.blink()
  1670.             return
  1671.  
  1672.         delegate.openExternalURL(url)
  1673.  
  1674.     @eventloop.asUrgent
  1675.     def dispatchAction(self, action, **kwargs):
  1676.         called = False
  1677.         start = clock()
  1678.         for handler in self.actionHandlers:
  1679.             if hasattr(handler, action):
  1680.                 getattr(handler, action)(**kwargs)
  1681.                 called = True
  1682.                 break
  1683.         end = clock()
  1684.         if end - start > 0.5:
  1685.             logging.timing ("dispatch action %s too slow (%.3f secs)", action, end - start)
  1686.         if not called:
  1687.             logging.warning ("Ignored bad action URL: action=%s", action)
  1688.  
  1689.     @eventloop.asUrgent
  1690.     def onDeselected(self, frame):
  1691.         unloadTriggers = self.templateHandle.getTriggerActionURLsOnUnload()
  1692.         self.runActionURLs(unloadTriggers)
  1693.         self.unlink()
  1694.         frontend.HTMLDisplay.onDeselected(self, frame)
  1695.  
  1696.     def unlink(self):
  1697.         self.templateHandle.unlinkTemplate()
  1698.         self.actionHandlers = []
  1699.  
  1700. ###############################################################################
  1701. #### Handlers for actions generated from templates, the OS, etc            ####
  1702. ###############################################################################
  1703.  
  1704. # Functions that are safe to call from action: URLs that do nothing
  1705. # but manipulate the database.
  1706. class ModelActionHandler:
  1707.     
  1708.     def __init__(self, backEndDelegate):
  1709.         self.backEndDelegate = backEndDelegate
  1710.     
  1711.     def setAutoDownloadMode(self, feed, mode):
  1712.         obj = db.getObjectByID(int(feed))
  1713.         obj.setAutoDownloadMode(mode)
  1714.  
  1715.     def setExpiration(self, feed, type, time):
  1716.         obj = db.getObjectByID(int(feed))
  1717.         obj.setExpiration(type, int(time))
  1718.  
  1719.     def setMaxNew(self, feed, maxNew):
  1720.         obj = db.getObjectByID(int(feed))
  1721.         obj.setMaxNew(int(maxNew))
  1722.  
  1723.     def invalidMaxNew(self, value):
  1724.         title = _("Invalid Value")
  1725.         description = _("%s is invalid.  You must enter a non-negative "
  1726.                 "number.") % value
  1727.         dialogs.MessageBoxDialog(title, description).run()
  1728.  
  1729.     def startDownload(self, item):
  1730.         try:
  1731.             obj = db.getObjectByID(int(item))
  1732.             obj.download()
  1733.         except database.ObjectNotFoundError:
  1734.             pass
  1735.  
  1736.     def removeFeed(self, id):
  1737.         try:
  1738.             feed = db.getObjectByID(int(id))
  1739.             controller.removeFeed(feed)
  1740.         except database.ObjectNotFoundError:
  1741.             pass
  1742.  
  1743.     def removeCurrentFeed(self):
  1744.         controller.removeCurrentFeed()
  1745.  
  1746.     def removeCurrentPlaylist(self):
  1747.         controller.removeCurrentPlaylist()
  1748.  
  1749.     def removeCurrentItems(self):
  1750.         controller.removeCurrentItems()
  1751.  
  1752.     def mergeToFolder(self):
  1753.         tls = controller.selection.tabListSelection
  1754.         selectionType = tls.getType()
  1755.         childIDs = set(tls.currentSelection)
  1756.         if selectionType == 'channeltab':
  1757.             folder.createNewChannelFolder(childIDs)
  1758.         elif selectionType == 'playlisttab':
  1759.             folder.createNewPlaylistFolder(childIDs)
  1760.         else:
  1761.             logging.warning ("bad selection type %s in mergeToFolder",
  1762.                              selectionType)
  1763.  
  1764.     def remove(self, area, id):
  1765.         selectedIDs = controller.selection.calcSelection(area, int(id))
  1766.         selectedObjects = [db.getObjectByID(id) for id in selectedIDs]
  1767.         objType = selectedObjects[0].__class__
  1768.  
  1769.         if objType in (feed.Feed, folder.ChannelFolder):
  1770.             controller.removeFeeds(selectedObjects)
  1771.         elif objType in (playlist.SavedPlaylist, folder.PlaylistFolder):
  1772.             controller.removePlaylists(selectedObjects)
  1773.         elif objType == guide.ChannelGuide:
  1774.             if len(selectedObjects) != 1:
  1775.                 raise AssertionError("Multiple guides selected in remove")
  1776.             controller.removeGuide(selectedObjects[0])
  1777.         elif objType == item.Item:
  1778.             pl = controller.selection.getSelectedTabs()[0].obj
  1779.             pl.handleRemove(destObj, selectedIDs)
  1780.         else:
  1781.             logging.warning ("Can't handle type %s in remove()", objType)
  1782.  
  1783.     def rename(self, id):
  1784.         try:
  1785.             obj = db.getObjectByID(int(id))
  1786.         except:
  1787.             logging.warning ("tried to rename object that doesn't exist with id %d", int(feed))
  1788.             return
  1789.         if obj.__class__ in (playlist.SavedPlaylist, folder.ChannelFolder,
  1790.                 folder.PlaylistFolder):
  1791.             obj.rename()
  1792.         else:
  1793.             logging.warning ("Unknown object type in remove() %s", type(obj))
  1794.  
  1795.     def updateFeed(self, feed):
  1796.         obj = db.getObjectByID(int(feed))
  1797.         obj.update()
  1798.  
  1799.     def copyFeedURL(self, feed):
  1800.         obj = db.getObjectByID(int(feed))
  1801.         url = obj.getURL()
  1802.         self.backEndDelegate.copyTextToClipboard(url)
  1803.  
  1804.     def markFeedViewed(self, feed):
  1805.         try:
  1806.             obj = db.getObjectByID(int(feed))
  1807.             obj.markAsViewed()
  1808.         except database.ObjectNotFoundError:
  1809.             pass
  1810.  
  1811.     def updateIcons(self, feed):
  1812.         try:
  1813.             obj = db.getObjectByID(int(feed))
  1814.             obj.updateIcons()
  1815.         except database.ObjectNotFoundError:
  1816.             pass
  1817.  
  1818.     def expireItem(self, item):
  1819.         try:
  1820.             obj = db.getObjectByID(int(item))
  1821.             obj.expire()
  1822.         except database.ObjectNotFoundError:
  1823.             logging.warning ("tried to expire item that doesn't exist with id %d", int(item))
  1824.  
  1825.     def expirePlayingItem(self, item):
  1826.         self.expireItem(item)
  1827.         controller.playbackController.skip(1)
  1828.  
  1829.     def addItemToLibrary(self, item):
  1830.         obj = db.getObjectByID(int(item))
  1831.         manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1832.         obj.setFeed(manualFeed.getID())
  1833.  
  1834.     def keepItem(self, item):
  1835.         obj = db.getObjectByID(int(item))
  1836.         obj.save()
  1837.  
  1838.     def stopUploadItem(self, item):
  1839.         obj = db.getObjectByID(int(item))
  1840.         obj.stopUpload()
  1841.  
  1842.     def toggleMoreItemInfo(self, item):
  1843.         obj = db.getObjectByID(int(item))
  1844.         obj.toggleShowMoreInfo()
  1845.  
  1846.     def revealItem(self, item):
  1847.         obj = db.getObjectByID(int(item))
  1848.         filename = obj.getFilename()
  1849.         if not os.path.exists(filename):
  1850.             basename = os.path.basename(filename)
  1851.             title = _("Error Revealing File")
  1852.             msg = _("The file \"%s\" was deleted from outside Miro.") % basename
  1853.             dialogs.MessageBoxDialog(title, msg).run()
  1854.         else:
  1855.             self.backEndDelegate.revealFile(filename)
  1856.  
  1857.     def clearTorrents (self):
  1858.         items = views.items.filter(lambda x: x.getFeed().url == u'dtv:manualFeed' and x.isNonVideoFile() and not x.getState() == u"downloading")
  1859.         for i in items:
  1860.             if i.downloader is not None:
  1861.                 i.downloader.setDeleteFiles(False)
  1862.             i.remove()
  1863.  
  1864.     def pauseDownload(self, item):
  1865.         obj = db.getObjectByID(int(item))
  1866.         obj.pause()
  1867.         
  1868.     def resumeDownload(self, item):
  1869.         obj = db.getObjectByID(int(item))
  1870.         obj.resume()
  1871.  
  1872.     def pauseAll (self):
  1873.         autodler.pauseDownloader()
  1874.         for item in views.downloadingItems:
  1875.             item.pause()
  1876.  
  1877.     def resumeAll (self):
  1878.         for item in views.pausedItems:
  1879.             item.resume()
  1880.         autodler.resumeDownloader()
  1881.  
  1882.     def toggleExpand(self, id):
  1883.         obj = db.getObjectByID(int(id))
  1884.         obj.setExpanded(not obj.getExpanded())
  1885.  
  1886.     def setRunAtStartup(self, value):
  1887.         value = (value == "1")
  1888.         self.backEndDelegate.setRunAtStartup(value)
  1889.  
  1890.     def setCheckEvery(self, value):
  1891.         value = int(value)
  1892.         config.set(prefs.CHECK_CHANNELS_EVERY_X_MN,value)
  1893.  
  1894.     def setLimitUpstream(self, value):
  1895.         value = (value == "1")
  1896.         config.set(prefs.LIMIT_UPSTREAM,value)
  1897.  
  1898.     def setMaxUpstream(self, value):
  1899.         value = int(value)
  1900.         config.set(prefs.UPSTREAM_LIMIT_IN_KBS,value)
  1901.  
  1902.     def setPreserveDiskSpace(self, value):
  1903.         value = (value == "1")
  1904.         config.set(prefs.PRESERVE_DISK_SPACE,value)
  1905.  
  1906.     def setDefaultExpiration(self, value):
  1907.         value = int(value)
  1908.         config.set(prefs.EXPIRE_AFTER_X_DAYS,value)
  1909.  
  1910.     def videoBombExternally(self, item):
  1911.         obj = db.getObjectByID(int(item))
  1912.         paramList = {}
  1913.         paramList["title"] = obj.getTitle()
  1914.         paramList["info_url"] = obj.getLink()
  1915.         paramList["hookup_url"] = obj.getPaymentLink()
  1916.         try:
  1917.             rss_url = obj.getFeed().getURL()
  1918.             if (not rss_url.startswith(u'dtv:')):
  1919.                 paramList["rss_url"] = rss_url
  1920.         except:
  1921.             pass
  1922.         thumb_url = obj.getThumbnailURL()
  1923.         if thumb_url is not None:
  1924.             paramList["thumb_url"] = thumb_url
  1925.  
  1926.         # FIXME: add "explicit" and "tags" parameters when we get them in item
  1927.  
  1928.         paramString = ""
  1929.         glue = '?'
  1930.        
  1931.         # This should be first, since it's most important.
  1932.         url = obj.getURL()
  1933.         url.encode('utf-8', 'replace')
  1934.         if (not url.startswith('file:')):
  1935.             paramString = "?url=%s" % xhtmltools.urlencode(url)
  1936.             glue = '&'
  1937.  
  1938.         for key in paramList.keys():
  1939.             if len(paramList[key]) > 0:
  1940.                 paramString = "%s%s%s=%s" % (paramString, glue, key, xhtmltools.urlencode(paramList[key]))
  1941.                 glue = '&'
  1942.  
  1943.         # This should be last, so that if it's extra long it 
  1944.         # cut off all the other parameters
  1945.         description = obj.getDescription()
  1946.         if len(description) > 0:
  1947.             paramString = "%s%sdescription=%s" % (paramString, glue,
  1948.                     xhtmltools.urlencode(description))
  1949.         url = config.get(prefs.VIDEOBOMB_URL) + paramString
  1950.         self.backEndDelegate.openExternalURL(url)
  1951.  
  1952.     def changeMoviesDirectory(self, newDir, migrate):
  1953.         changeMoviesDirectory(newDir, migrate == '1')
  1954.  
  1955. # Test shim for test* functions on GUIActionHandler
  1956. class printResultThread(threading.Thread):
  1957.  
  1958.     def __init__(self, format, func):
  1959.         self.format = format
  1960.         self.func = func
  1961.         threading.Thread.__init__(self)
  1962.  
  1963.     def run(self):
  1964.         print (self.format % (self.func(), ))
  1965.  
  1966. # Functions that are safe to call from action: URLs that can change
  1967. # the GUI presentation (and may or may not manipulate the database.)
  1968. class GUIActionHandler:
  1969.  
  1970.     def playUnwatched(self):
  1971.         controller.playView(views.unwatchedItems)
  1972.  
  1973.     def openFile(self, path):
  1974.         singleclick.openFile(path)
  1975.  
  1976.     def addSearchFeed(self, term=None, style = dialogs.SearchChannelDialog.CHANNEL, location = None):
  1977.         def doAdd(dialog):
  1978.             if dialog.choice == dialogs.BUTTON_CREATE_CHANNEL:
  1979.                 self.addFeed(dialog.getURL())
  1980.         dialog = dialogs.SearchChannelDialog(term, style, location)
  1981.         if location == None:
  1982.             dialog.run(doAdd)
  1983.         else:
  1984.             self.addFeed(dialog.getURL())
  1985.  
  1986.     def addChannelSearchFeed(self, id):
  1987.         feed = db.getObjectByID(int(id))
  1988.         self.addSearchFeed(feed.inlineSearchTerm, dialogs.SearchChannelDialog.CHANNEL, int(id))
  1989.  
  1990.     def addEngineSearchFeed(self, term, name):
  1991.         self.addSearchFeed(term, dialogs.SearchChannelDialog.ENGINE, name)
  1992.         
  1993.     def testSearchFeedDialog(self):
  1994.         def finish(dialog):
  1995.             pass
  1996.         def thirdDialog(dialog):
  1997.             dialog = dialogs.SearchChannelDialog("Should select URL http://testurl/", dialogs.SearchChannelDialog.URL, "http://testurl/")
  1998.             dialog.run(finish)
  1999.         def secondDialog(dialog):
  2000.             dialog = dialogs.SearchChannelDialog("Should select YouTube engine", dialogs.SearchChannelDialog.ENGINE, "youtube")
  2001.             dialog.run(thirdDialog)
  2002.         dialog = dialogs.SearchChannelDialog("Should select third channel in list", dialogs.SearchChannelDialog.CHANNEL, -1)
  2003.         dialog.run(secondDialog)
  2004.         
  2005.     def addURL(self, title, message, callback, url = None):
  2006.         util.checkU(url)
  2007.         util.checkU(title)
  2008.         util.checkU(message)
  2009.         def createDialog(ltitle, lmessage, prefill = None):
  2010.             def prefillCallback():
  2011.                 if prefill:
  2012.                     return prefill
  2013.                 else:
  2014.                     return None
  2015.             dialog = dialogs.TextEntryDialog(ltitle, lmessage, dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL, prefillCallback, fillWithClipboardURL=(prefill is None))
  2016.             def callback(dialog):
  2017.                 if dialog.choice == dialogs.BUTTON_OK:
  2018.                     doAdd(dialog.value)
  2019.             dialog.run(callback)
  2020.         def doAdd(url):
  2021.             normalizedURL = feed.normalizeFeedURL(url)
  2022.             if not feed.validateFeedURL(normalizedURL):
  2023.                 ltitle = title + _(" - Invalid URL")
  2024.                 lmessage = _("The address you entered is not a valid URL.\nPlease double check and try again.\n\n") + message
  2025.                 createDialog(ltitle, lmessage, url)
  2026.                 return
  2027.             callback(normalizedURL)
  2028.         if url is None:
  2029.             createDialog(title, message)
  2030.         else:
  2031.             doAdd(url)
  2032.         
  2033.     # NEEDS: name should change to addAndSelectFeed; then we should create
  2034.     # a non-GUI addFeed to match removeFeed. (requires template updates)
  2035.     def addFeed(self, url = None, showTemplate = None, selected = '1'):
  2036.         if url:
  2037.             util.checkU(url)
  2038.         def doAdd (url):
  2039.             db.confirmDBThread()
  2040.             myFeed = feed.getFeedByURL (url)
  2041.             if myFeed is None:
  2042.                 myFeed = feed.Feed(url)
  2043.     
  2044.             if selected == '1':
  2045.                 controller.selection.selectTabByObject(myFeed)
  2046.             else:
  2047.                 myFeed.blink()
  2048.         self.addURL (Template(_("$shortAppName - Add Channel")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the channel to add"), doAdd, url)
  2049.  
  2050.     def selectFeed(self, url):
  2051.         url = feed.normalizeFeedURL(url)
  2052.         db.confirmDBThread()
  2053.         # Find the feed
  2054.         myFeed = feed.getFeedByURL (url)
  2055.         if myFeed is None:
  2056.             logging.warning ("selectFeed: no such feed: %s", url)
  2057.             return
  2058.         controller.selection.selectTabByObject(myFeed)
  2059.         
  2060.     def addGuide(self, url = None, selected = '1'):
  2061.         def doAdd(url):
  2062.             db.confirmDBThread()
  2063.             myGuide = guide.getGuideByURL (url)
  2064.             if myGuide is None:
  2065.                 myGuide = guide.ChannelGuide(url)
  2066.     
  2067.             if selected == '1':
  2068.                 controller.selection.selectTabByObject(myGuide)
  2069.         self.addURL (Template(_("$shortAppName - Add Miro Guide")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the Miro Guide to add"), doAdd, url)
  2070.  
  2071.     def addDownload(self, url = None):
  2072.         def doAdd(url):
  2073.             db.confirmDBThread()
  2074.             singleclick.downloadURL(platformutils.unicodeToFilename(url))
  2075.         self.addURL (Template(_("$shortAppName - Download Video")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the video to download"), doAdd, url)
  2076.  
  2077.     def handleDrop(self, data, type, sourcedata):
  2078.         controller.handleDrop(data, type, sourcedata)
  2079.  
  2080.     def handleURIDrop(self, data, **kwargs):
  2081.         controller.handleURIDrop(data, **kwargs)
  2082.  
  2083.     def showHelp(self):
  2084.         delegate.openExternalURL(config.get(prefs.HELP_URL))
  2085.  
  2086.     def reportBug(self):
  2087.         delegate.openExternalURL(config.get(prefs.BUG_REPORT_URL))
  2088.  
  2089. # Functions that are safe to call from action: URLs that change state
  2090. # specific to a particular instantiation of a template, and so have to
  2091. # be scoped to a particular HTML display widget.
  2092. class TemplateActionHandler:
  2093.     
  2094.     def __init__(self, display, templateHandle):
  2095.         self.display = display
  2096.         self.templateHandle = templateHandle
  2097.         self.currentName = None
  2098.  
  2099.     def switchTemplate(self, name, state='default', baseURL=None, *args, **kargs):
  2100.         self.templateHandle.unlinkTemplate()
  2101.         # Switch to new template. It get the same variable
  2102.         # dictionary as we have.
  2103.         # NEEDS: currently we hardcode the display area. This means
  2104.         # that these links always affect the right-hand 'content'
  2105.         # area, even if they are loaded from the left-hand 'tab'
  2106.         # area. Actually this whole invocation is pretty hacky.
  2107.         template = TemplateDisplay(name, state, frameHint=controller.frame,
  2108.                 areaHint=controller.frame.mainDisplay, baseURL=baseURL,
  2109.                 *args, **kargs)
  2110.         controller.frame.selectDisplay(template, controller.frame.mainDisplay)
  2111.         self.currentName = name
  2112.  
  2113.     def setViewFilter(self, viewName, fieldKey, functionKey, parameter, invert):
  2114.         logging.warning ("setViewFilter deprecated")
  2115.  
  2116.     def setViewSort(self, viewName, fieldKey, functionKey, reverse="false"):
  2117.         logging.warning ("setViewSort deprecated")
  2118.  
  2119.     def setSearchString(self, searchString):
  2120.         try:
  2121.             self.templateHandle.getTemplateVariable('updateSearchString')(unicode(searchString))
  2122.         except KeyError, e:
  2123.             logging.warning ("KeyError in getTemplateVariable ('updateSearchString')")
  2124.  
  2125.     def toggleDownloadsView(self):
  2126.         try:
  2127.             self.templateHandle.getTemplateVariable('toggleDownloadsView')(self.templateHandle)
  2128.         except KeyError, e:
  2129.             logging.warning ("KeyError in getTemplateVariable ('toggleDownloadsView')")
  2130.  
  2131.     def toggleWatchableView(self):
  2132.         try:
  2133.             self.templateHandle.getTemplateVariable('toggleWatchableView')(self.templateHandle)
  2134.         except KeyError, e:
  2135.             logging.warning ("KeyError in getTemplateVariable ('toggleWatchableView')")
  2136.  
  2137.     def toggleNewItemsView(self):
  2138.         try:
  2139.             self.templateHandle.getTemplateVariable('toggleNewItemsView')(self.templateHandle)
  2140.         except KeyError, e:
  2141.             logging.warning ("KeyError in getTemplateVariable ('toggleNewItemsView')")            
  2142.  
  2143.     def toggleAllItemsMode(self):
  2144.         try:
  2145.             self.templateHandle.getTemplateVariable('toggleAllItemsMode')(self.templateHandle)
  2146.         except KeyError, e:
  2147.             logging.warning ("KeyError in getTemplateVariable ('toggleAllItemsMode')")
  2148.  
  2149.     def pauseDownloads(self):
  2150.         try:
  2151.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2152.         except KeyError, e:
  2153.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during pauseDownloads()")
  2154.             return
  2155.         for item in view:
  2156.             item.pause()
  2157.  
  2158.     def resumeDownloads(self):
  2159.         try:
  2160.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2161.         except KeyError, e:
  2162.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during resumeDownloads()")
  2163.             return
  2164.         for item in view:
  2165.             item.resume()
  2166.  
  2167.     def cancelDownloads(self):
  2168.         try:
  2169.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2170.         except KeyError, e:
  2171.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during cancelDownloads()")
  2172.             return
  2173.         for item in view:
  2174.             item.expire()
  2175.  
  2176.     def playViewNamed(self, viewName, firstItemId):
  2177.         try:
  2178.             view = self.templateHandle.getTemplateVariable(viewName)
  2179.         except KeyError, e:
  2180.             logging.warning ("KeyError in getTemplateVariable (%s) during playViewNamed()" % (viewName,))
  2181.             return
  2182.         controller.playView(view, firstItemId)
  2183.  
  2184.     def playOneItem(self, viewName, itemID):
  2185.         try:
  2186.             view = self.templateHandle.getTemplateVariable(viewName)
  2187.         except KeyError, e:
  2188.             logging.warning ("KeyError in getTemplateVariable (%s) during playOneItem()" % (viewName,))
  2189.             return
  2190.         controller.playView(view, itemID, justPlayOne=True)
  2191.  
  2192.     def playNewVideos(self, id):
  2193.         try:
  2194.             obj = db.getObjectByID(int(id))
  2195.         except database.ObjectNotFoundError:
  2196.             return
  2197.  
  2198.         def myUnwatchedItems(obj):
  2199.             return (obj.getState() == u'newly-downloaded' and
  2200.                     not obj.isNonVideoFile() and
  2201.                     not obj.isContainerItem)
  2202.  
  2203.         controller.selection.selectTabByObject(obj, displayTabContent=False)
  2204.         if isinstance(obj, feed.Feed):
  2205.             feedView = views.items.filterWithIndex(indexes.itemsByFeed,
  2206.                     obj.getID())
  2207.             view = feedView.filter(myUnwatchedItems,
  2208.                                    sortFunc=sorts.item)
  2209.             controller.playView(view)
  2210.             view.unlink()
  2211.         elif isinstance(obj, folder.ChannelFolder):
  2212.             folderView = views.items.filterWithIndex(
  2213.                     indexes.itemsByChannelFolder, obj)
  2214.             view = folderView.filter(myUnwatchedItems,
  2215.                                      sortFunc=sorts.item)
  2216.             controller.playView(view)
  2217.             view.unlink()
  2218.         elif isinstance(obj, tabs.StaticTab): # new videos tab
  2219.             view = views.unwatchedItems
  2220.             controller.playView(view)
  2221.         else:
  2222.             raise TypeError("Can't get new videos for %s (type: %s)" % 
  2223.                     (obj, type(obj)))
  2224.  
  2225.     def playItemExternally(self, itemID):
  2226.         controller.playbackController.playItemExternally(itemID)
  2227.         
  2228.     def skipItem(self, itemID):
  2229.         controller.playbackController.skip(1)
  2230.     
  2231.     def updateLastSearchEngine(self, engine):
  2232.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2233.         if searchFeed is not None:
  2234.             searchFeed.lastEngine = engine
  2235.     
  2236.     def updateLastSearchQuery(self, query):
  2237.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2238.         if searchFeed is not None:
  2239.             searchFeed.lastQuery = query
  2240.         
  2241.     def performSearch(self, engine, query):
  2242.         util.checkU(engine)
  2243.         util.checkU(query)
  2244.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2245.         if searchFeed is not None and searchDownloadsFeed is not None:
  2246.             searchFeed.preserveDownloads(searchDownloadsFeed)
  2247.             searchFeed.lookup(engine, query)
  2248.  
  2249.     def resetSearch(self):
  2250.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2251.         if searchFeed is not None and searchDownloadsFeed is not None:
  2252.             searchFeed.preserveDownloads(searchDownloadsFeed)
  2253.             searchFeed.reset()
  2254.  
  2255.     def sortBy(self, by, section):
  2256.         try:
  2257.             self.templateHandle.getTemplateVariable('setSortBy')(by, section, self.templateHandle)
  2258.         except KeyError, e:
  2259.             logging.warning ("KeyError in getTemplateVariable ('setSortBy')")
  2260.  
  2261.     def handleSelect(self, area, viewName, id, shiftDown, ctrlDown):
  2262.         try:
  2263.             view = self.templateHandle.getTemplateVariable(viewName)
  2264.         except KeyError, e: # user switched templates before we got this
  2265.             logging.warning ("KeyError in getTemplateVariable (%s) during handleSelect()" % (viewName,))
  2266.             return
  2267.         shift = (shiftDown == '1')
  2268.         ctrl = (ctrlDown == '1')
  2269.         controller.selection.selectItem(area, view, int(id), shift, ctrl)
  2270.  
  2271.     def handleContextMenuSelect(self, id, area, viewName):
  2272.         try:
  2273.             obj = db.getObjectByID(int(id))
  2274.         except:
  2275.             traceback.print_exc()
  2276.         else:
  2277.             try:
  2278.                 view = self.templateHandle.getTemplateVariable(viewName)
  2279.             except KeyError, e: # user switched templates before we got this
  2280.                 logging.warning ("KeyError in getTemplateVariable (%s) during handleContextMenuSelect()" % (viewName,))
  2281.                 return
  2282.             if not controller.selection.isSelected(area, view, int(id)):
  2283.                 self.handleSelect(area, viewName, id, False, False)
  2284.             popup = menu.makeContextMenu(self.currentName, view,
  2285.                     controller.selection.getSelectionForArea(area), int(id))
  2286.             if popup:
  2287.                 delegate.showContextMenu(popup)
  2288.  
  2289.     def __getSearchFeeds(self):
  2290.         searchFeed = controller.getGlobalFeed('dtv:search')
  2291.         assert searchFeed is not None
  2292.         
  2293.         searchDownloadsFeed = controller.getGlobalFeed('dtv:searchDownloads')
  2294.         assert searchDownloadsFeed is not None
  2295.  
  2296.         return (searchFeed, searchDownloadsFeed)
  2297.  
  2298.     # The Windows XUL port can send a setVolume or setVideoProgress at
  2299.     # any time, even when there's no video display around. We can just
  2300.     # ignore it
  2301.     def setVolume(self, level):
  2302.         pass
  2303.     def setVideoProgress(self, pos):
  2304.         pass
  2305.  
  2306. # Helper: liberally interpret the provided string as a boolean
  2307. def stringToBoolean(string):
  2308.     if string == "" or string == "0" or string == "false":
  2309.         return False
  2310.     else:
  2311.         return True
  2312.  
  2313. ###############################################################################
  2314. #### Playlist & Video clips                                                ####
  2315. ###############################################################################
  2316.  
  2317. class Playlist:
  2318.     
  2319.     def __init__(self, view, firstItemId):
  2320.         self.initialView = view
  2321.         self.filteredView = self.initialView.filter(mappableToPlaylistItem)
  2322.         self.view = self.filteredView.map(mapToPlaylistItem)
  2323.  
  2324.         # Move the cursor to the requested item; if there's no
  2325.         # such item in the view, move the cursor to the first
  2326.         # item
  2327.         self.view.confirmDBThread()
  2328.         self.view.resetCursor()
  2329.         while True:
  2330.             cur = self.view.getNext()
  2331.             if cur == None:
  2332.                 # Item not found in view. Put cursor at the first
  2333.                 # item, if any.
  2334.                 self.view.resetCursor()
  2335.                 self.view.getNext()
  2336.                 break
  2337.             if firstItemId is None or cur.getID() == int(firstItemId):
  2338.                 # The cursor is now on the requested item.
  2339.                 break
  2340.  
  2341.     def reset(self):
  2342.         self.initialView.removeView(self.filteredView)
  2343.         self.initialView = None
  2344.         self.filteredView = None
  2345.         self.view = None
  2346.  
  2347.     def cur(self):
  2348.         return self.itemMarkedAsViewed(self.view.cur())
  2349.  
  2350.     def getNext(self):
  2351.         return self.itemMarkedAsViewed(self.view.getNext())
  2352.         
  2353.     def getPrev(self):
  2354.         return self.itemMarkedAsViewed(self.view.getPrev())
  2355.  
  2356.     def itemMarkedAsViewed(self, anItem):
  2357.         if anItem is not None:
  2358.             eventloop.addIdle(anItem.onViewed, "Mark item viewed")
  2359.         return anItem
  2360.  
  2361. class PlaylistItemFromItem:
  2362.  
  2363.     def __init__(self, anItem):
  2364.         self.item = anItem
  2365.         self.dcOnViewed = None
  2366.  
  2367.     def getTitle(self):
  2368.         return self.item.getTitle()
  2369.  
  2370.     def getVideoFilename(self):
  2371.         return self.item.getVideoFilename()
  2372.  
  2373.     def getLength(self):
  2374.         # NEEDS
  2375.         return 42.42
  2376.  
  2377.     def onViewedExecute(self):
  2378.         if self.item.idExists():
  2379.             self.item.markItemSeen()
  2380.         self.dcOnViewed = None
  2381.  
  2382.     def onViewed(self):
  2383.         if self.dcOnViewed or self.item.getSeen():
  2384.             return
  2385.         self.dcOnViewed = eventloop.addTimeout(5, self.onViewedExecute, "Mark item viewed")
  2386.  
  2387.     def onViewedCancel(self):
  2388.         if self.dcOnViewed:
  2389.             self.dcOnViewed.cancel()
  2390.             self.dcOnViewed = None
  2391.  
  2392.     # Return the ID that is used by a template to indicate this item 
  2393.     def getID(self):
  2394.         return self.item.getID()
  2395.  
  2396.     def __getattr__(self, attr):
  2397.         return getattr(self.item, attr)
  2398.  
  2399. def mappableToPlaylistItem(obj):
  2400.     return (isinstance(obj, item.Item) and obj.isDownloaded())
  2401.  
  2402. def mapToPlaylistItem(obj):
  2403.     return PlaylistItemFromItem(obj)
  2404.  
  2405. def _defaultFeeds():
  2406.     if config.get(prefs.DEFAULT_CHANNELS_FILE) is not None:
  2407.         importer = opml.Importer()
  2408.         try:
  2409.             if ((config.get(prefs.THEME_NAME) is not None) and 
  2410.                 (config.get(prefs.THEME_DIRECTORY) is not None)):
  2411.                 filepath = os.path.join(
  2412.                     config.get(prefs.THEME_DIRECTORY),
  2413.                     config.get(prefs.THEME_NAME),
  2414.                     config.get(prefs.DEFAULT_CHANNELS_FILE))
  2415.             else:
  2416.                 filepath = os.path.join(
  2417.                     config.get(prefs.SUPPORT_DIRECTORY),
  2418.                     config.get(prefs.DEFAULT_CHANNELS_FILE))
  2419.             importer.importSubscriptionsFrom(filepath,
  2420.                                              showSummary = False)
  2421.             logging.info("Imported %s" % filepath)
  2422.         except:
  2423.             logging.warn("Could not import %s" % filepath)
  2424.         return
  2425.     logging.info("Adding default feeds")
  2426.     if platform.system() == 'Darwin':
  2427.         defaultFeedURLs = [u'http://www.getmiro.com/screencasts/mac/mac.feed.rss']
  2428.     elif platform.system() == 'Windows':
  2429.         defaultFeedURLs = [u'http://www.getmiro.com/screencasts/windows/win.feed.rss']
  2430.     else:
  2431.         defaultFeedURLs = [u'http://www.getmiro.com/screencasts/windows/win.feed.rss']
  2432.     defaultFeedURLs.extend([ (_('Starter Channels'),
  2433.                               [u'http://richie-b.blip.tv/posts/?skin=rss',
  2434.                                u'http://feeds.pbs.org/pbs/kcet/wiredscience-video',
  2435.                                u'http://www.jpl.nasa.gov/multimedia/rss/podfeed-hd.xml',
  2436.                                u'http://www.linktv.org/rss/hq/mosaic.xml']),
  2437.                            ])
  2438.  
  2439.     for default in defaultFeedURLs:
  2440.         print repr(default)
  2441.         if isinstance(default, tuple): # folder
  2442.             defaultFolder = default
  2443.             c_folder = folder.ChannelFolder(defaultFolder[0])
  2444.             for url in defaultFolder[1]:
  2445.                 d_feed = feed.Feed(url, initiallyAutoDownloadable=False)
  2446.                 d_feed.setFolder(c_folder)
  2447.         else: # feed
  2448.             d_feed = feed.Feed(default, initiallyAutoDownloadable=False)
  2449.     playlist.SavedPlaylist(_(u"Example Playlist"))
  2450.  
  2451. def _getThemeHistory():
  2452.     if len(views.themeHistories) > 0:
  2453.         return views.themeHistories[0]
  2454.     else:
  2455.         return theme.ThemeHistory()
  2456.  
  2457. def _getInitialChannelGuide():
  2458.     default_guide = None
  2459.     newGuide = False
  2460.     for guideObj in views.guides:
  2461.         if default_guide is None:
  2462.             if guideObj.getDefault():
  2463.                 default_guide = guideObj
  2464.  
  2465.     if default_guide is None:
  2466.         newGuide = True
  2467.         logging.info ("Spawning Miro Guide...")
  2468.         default_guide = guide.ChannelGuide()
  2469.         initialFeeds = resources.path("initial-feeds.democracy")
  2470.         if os.path.exists(initialFeeds):
  2471.             urls = subscription.parseFile(initialFeeds)
  2472.             if urls is not None:
  2473.                 for url in urls:
  2474.                     feed.Feed(url, initiallyAutoDownloadable=False)
  2475.             dialog = dialogs.MessageBoxDialog(_("Custom Channels"), Template(_("You are running a version of $longAppName with a custom set of channels.")).substitute(longAppName=config.get(prefs.LONG_APP_NAME)))
  2476.             dialog.run()
  2477.             controller.initial_feeds = True
  2478.         else:
  2479.             _defaultFeeds()
  2480.     return (newGuide, default_guide)
  2481.  
  2482. # Race conditions:
  2483.  
  2484. # We do the migration in the dl_daemon if the dl_daemon knows about it
  2485. # so that we don't get a race condition.
  2486.  
  2487. @eventloop.asUrgent
  2488. def changeMoviesDirectory(newDir, migrate):
  2489.     if not util.directoryWritable(newDir):
  2490.         dialog = dialogs.MessageBoxDialog(_("Error Changing Movies Directory"), 
  2491.                 _("You don't have permission to write to the directory you selected.  Miro will continue to use the old videos directory."))
  2492.         dialog.run()
  2493.         return
  2494.  
  2495.     oldDir = config.get(prefs.MOVIES_DIRECTORY)
  2496.     config.set(prefs.MOVIES_DIRECTORY, newDir)
  2497.     if migrate:
  2498.         views.remoteDownloads.confirmDBThread()
  2499.         for download in views.remoteDownloads:
  2500.             if download.isFinished():
  2501.                 logging.info ("migrating %s", download.getFilename())
  2502.                 download.migrate(newDir)
  2503.         # Pass in case they don't exist or are not empty:
  2504.         try:
  2505.             os.rmdir(os.path.join (oldDir, 'Incomplete Downloads'))
  2506.         except:
  2507.             pass
  2508.         try:
  2509.             os.rmdir(oldDir)
  2510.         except:
  2511.             pass
  2512.     util.getSingletonDDBObject(views.directoryFeed).update()
  2513.  
  2514. @eventloop.asUrgent
  2515. def saveVideo(currentPath, savePath):
  2516.     logging.info("saving video %s to %s" % (currentPath, savePath))
  2517.     try:
  2518.         shutil.copyfile(currentPath, savePath)
  2519.     except:
  2520.         title = _('Error Saving Video')
  2521.         name = os.path.basename(currentPath)
  2522.         text = _('An error occured while trying to save %s.  Please check that the file has not been deleted and try again.') % util.clampText(name, 50)
  2523.         dialogs.MessageBoxDialog(title, text).run()
  2524.         logging.warn("Error saving video: %s" % traceback.format_exc())
  2525.